hanzo_client/apis/node_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 [`get_node`]
19#[derive(Debug, Clone, Serialize, Deserialize)]
20#[serde(untagged)]
21pub enum GetNodeError {
22 UnknownValue(serde_json::Value),
23}
24
25/// struct for typed errors of method [`get_node_connect`]
26#[derive(Debug, Clone, Serialize, Deserialize)]
27#[serde(untagged)]
28pub enum GetNodeConnectError {
29 UnknownValue(serde_json::Value),
30}
31
32/// struct for typed errors of method [`post_node_by_id_invoke`]
33#[derive(Debug, Clone, Serialize, Deserialize)]
34#[serde(untagged)]
35pub enum PostNodeByIdInvokeError {
36 UnknownValue(serde_json::Value),
37}
38
39/// struct for typed errors of method [`post_node_peer_invoke`]
40#[derive(Debug, Clone, Serialize, Deserialize)]
41#[serde(untagged)]
42pub enum PostNodePeerInvokeError {
43 UnknownValue(serde_json::Value),
44}
45
46
47/// Returns the caller org's currently connected bot nodes: what each one calls itself, the platform it runs on, its agent version, when its socket was established, and the capabilities and commands it reported. Only this org's nodes are listed — the org is half of every key in the table it reads — and only nodes attached to THIS replica, because the list is of live sockets rather than of registrations. The capability and command lists are the node's own self-report: useful to show, never load-bearing, because what a node may actually be asked to do is decided at the socket against the deployment's allowlist.
48pub async fn get_node(configuration: &configuration::Configuration, ) -> Result<models::NodesView, Error<GetNodeError>> {
49
50 let uri_str = format!("{}/v1/node", configuration.base_path);
51 let mut req_builder = configuration.client.request(reqwest::Method::GET, &uri_str);
52
53 if let Some(ref user_agent) = configuration.user_agent {
54 req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
55 }
56 if let Some(ref token) = configuration.bearer_access_token {
57 req_builder = req_builder.bearer_auth(token.to_owned());
58 };
59
60 let req = req_builder.build()?;
61 let resp = configuration.client.execute(req).await?;
62
63 let status = resp.status();
64 let content_type = resp
65 .headers()
66 .get("content-type")
67 .and_then(|v| v.to_str().ok())
68 .unwrap_or("application/octet-stream");
69 let content_type = super::ContentType::from(content_type);
70
71 if !status.is_client_error() && !status.is_server_error() {
72 let content = resp.text().await?;
73 match content_type {
74 ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
75 ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::NodesView`"))),
76 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::NodesView`")))),
77 }
78 } else {
79 let content = resp.text().await?;
80 let entity: Option<GetNodeError> = serde_json::from_str(&content).ok();
81 Err(Error::ResponseError(ResponseContent { status, content, entity }))
82 }
83}
84
85/// Upgrades to a WebSocket and keeps it for the life of the node. cloud writes a challenge frame immediately; the node answers with a connect frame naming the protocol range it speaks, the role `node`, its own node id, and the display name, platform, agent version, capabilities and commands it reports for itself. On acceptance the session is registered, the node appears in this org's node list, and invocations begin arriving as frames on the same connection. The upgrade needs a validated principal and answers 403 without one. The org is the gateway's verdict — injected after IAM validation and after any client copy is stripped — and is never read from the request itself, because a caller that could name an org could attach a machine into someone else's tenant. A request carrying an Origin header is refused outright. A node is a daemon and a browser has no business here; since no same-origin policy applies to WebSockets, a page could otherwise ride a signed-in viewer's session into registering a node. Removing the whole category is the gate, not an allowlist of brand domains. The handshake deadline is one fixed instant rather than a per-read timer, so a peer cannot hold a pre-handshake socket open indefinitely by sending frames this endpoint ignores. Two things to get right. Everything the node declares about itself — capabilities, commands, platform — is a SELF-REPORT: it is useful to show and never load-bearing, because what the node may actually be asked to run is decided at this socket against the deployment's allowlist. And a node can only ever answer calls placed on its own connection: correlation ids are minted under the connection id and checked against it, so naming another node's in-flight call resolves nothing.
86pub async fn get_node_connect(configuration: &configuration::Configuration, ) -> Result<(), Error<GetNodeConnectError>> {
87
88 let uri_str = format!("{}/v1/node/connect", configuration.base_path);
89 let mut req_builder = configuration.client.request(reqwest::Method::GET, &uri_str);
90
91 if let Some(ref user_agent) = configuration.user_agent {
92 req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
93 }
94 if let Some(ref token) = configuration.bearer_access_token {
95 req_builder = req_builder.bearer_auth(token.to_owned());
96 };
97
98 let req = req_builder.build()?;
99 let resp = configuration.client.execute(req).await?;
100
101 let status = resp.status();
102
103 if !status.is_client_error() && !status.is_server_error() {
104 Ok(())
105 } else {
106 let content = resp.text().await?;
107 let entity: Option<GetNodeConnectError> = serde_json::from_str(&content).ok();
108 Err(Error::ResponseError(ResponseContent { status, content, entity }))
109 }
110}
111
112/// Sends {command, params, timeoutMs, idempotencyKey} to the named node and answers with what the node returned: {ok, payload, code, message}, where payload is the node's own JSON passed through — cloud routes the call, it does not interpret the result. A reply that is not valid JSON becomes an empty payload rather than corrupting the response, which ok and code already qualify. Neither the node nor the org is a body field: the node is the path and the org is the caller's validated identity, and a field for either would be a field somebody could set to a stranger's. A validated principal is required (403 without one), and a node id that belongs to another org answers exactly like one that does not exist — not found — so this cannot be used to probe another tenant's fleet. Authorization happened ONCE, at the socket, on the replica holding that node — the only place that knows what the node declared it can do. A node attached to a different replica is reached through the peer forward and is authorized by the same code with the same session in hand, so a local node and a forwarded one cannot get different answers. The timeout defaults to 30s and is clamped to 5 minutes, so one request can never pin a node's socket open indefinitely. system.run is rewritten before dispatch: its approval control fields are re-derived from the approval record and whatever the caller claimed is discarded, because a caller that could pre-approve itself is the whole thing approvals exist to prevent. No approval registry is wired today, so an invocation CLAIMING an approval is refused while an ordinary one is unaffected. The one thing to get right: a refusal is a 403 carrying a DOMAIN body — {error, code, reason} — not the flat error envelope the rest of cloud returns, and the same body comes back whether the pre-flight sanitize refused it or the node's own gate did. Switch on `code`. The remaining failures are ordinary statuses: the node not answering in time is 504, and a node that disconnected or could not be reached is 502.
113pub async fn post_node_by_id_invoke(configuration: &configuration::Configuration, id: &str) -> Result<(), Error<PostNodeByIdInvokeError>> {
114 // add a prefix to parameters to efficiently prevent name collisions
115 let p_id = id;
116
117 let uri_str = format!("{}/v1/node/{id}/invoke", configuration.base_path, id=crate::apis::urlencode(p_id));
118 let mut req_builder = configuration.client.request(reqwest::Method::POST, &uri_str);
119
120 if let Some(ref user_agent) = configuration.user_agent {
121 req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
122 }
123 if let Some(ref token) = configuration.bearer_access_token {
124 req_builder = req_builder.bearer_auth(token.to_owned());
125 };
126
127 let req = req_builder.build()?;
128 let resp = configuration.client.execute(req).await?;
129
130 let status = resp.status();
131
132 if !status.is_client_error() && !status.is_server_error() {
133 Ok(())
134 } else {
135 let content = resp.text().await?;
136 let entity: Option<PostNodeByIdInvokeError> = serde_json::from_str(&content).ok();
137 Err(Error::ResponseError(ResponseContent { status, content, entity }))
138 }
139}
140
141/// A machine hop, not a caller-facing route. A node's socket lands on one replica while invocations land on any, so the replica that took the request forwards it here to the one that actually holds the node, and returns that answer as its own. It authenticates with the shared peer token, compared in constant time, and carries no user identity at all. That is why the org arrives IN THE BODY here: the forwarding replica already derived it from a gateway-validated header, so the value is a fact being relayed rather than a claim being made. On any caller-facing route the same field would be a cross-tenant invoke primitive. It fails closed on its own configuration: with no peer token set, or a half-wired cluster that has presence but no way to forward, it serves 503 and forwards nothing — an unauthenticated endpoint that takes an org from a body is precisely the hole. A missing or wrong token is 403, and the forwarded body is bounded. Two things to get right. Its refusals are text/plain rather than the JSON every zip error uses, so a client decoding them as JSON will fail on the error path only. And an invocation that RAN but was denied still answers 200 here, carrying a stable error token in the JSON body — no such node, timeout, node gone, denied, failed — which the calling replica maps back onto the status codes a caller sees. Authorization already ran on this replica at the socket and is deliberately not repeated.
142pub async fn post_node_peer_invoke(configuration: &configuration::Configuration, ) -> Result<(), Error<PostNodePeerInvokeError>> {
143
144 let uri_str = format!("{}/v1/node/peer/invoke", configuration.base_path);
145 let mut req_builder = configuration.client.request(reqwest::Method::POST, &uri_str);
146
147 if let Some(ref user_agent) = configuration.user_agent {
148 req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
149 }
150 if let Some(ref token) = configuration.bearer_access_token {
151 req_builder = req_builder.bearer_auth(token.to_owned());
152 };
153
154 let req = req_builder.build()?;
155 let resp = configuration.client.execute(req).await?;
156
157 let status = resp.status();
158
159 if !status.is_client_error() && !status.is_server_error() {
160 Ok(())
161 } else {
162 let content = resp.text().await?;
163 let entity: Option<PostNodePeerInvokeError> = serde_json::from_str(&content).ok();
164 Err(Error::ResponseError(ResponseContent { status, content, entity }))
165 }
166}
167