hanzo_client/apis/exec_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_exec_files_by_sid`]
19#[derive(Debug, Clone, Serialize, Deserialize)]
20#[serde(untagged)]
21pub enum GetExecFilesBySidError {
22 UnknownValue(serde_json::Value),
23}
24
25/// struct for typed errors of method [`post_exec`]
26#[derive(Debug, Clone, Serialize, Deserialize)]
27#[serde(untagged)]
28pub enum PostExecError {
29 UnknownValue(serde_json::Value),
30}
31
32/// struct for typed errors of method [`post_exec_programmatic`]
33#[derive(Debug, Clone, Serialize, Deserialize)]
34#[serde(untagged)]
35pub enum PostExecProgrammaticError {
36 Status501(),
37 UnknownValue(serde_json::Value),
38}
39
40/// struct for typed errors of method [`post_exec_upload`]
41#[derive(Debug, Clone, Serialize, Deserialize)]
42#[serde(untagged)]
43pub enum PostExecUploadError {
44 UnknownValue(serde_json::Value),
45}
46
47
48/// Lists the files in an execution session. Everything the session's sandbox holds — the uploads a run can read and the artifacts it produced — each then fetched from GET /v1/exec/download. The answer is a BARE JSON ARRAY of {name, lastModified}, where `name` is the same {session_id}/{fileId} identifier download takes, because that is what the client matches on. The obvious typed shape, `{files: […]}`, would have been a silent wire change: the request still succeeds and `response.data.find(...)` finds nothing, which reads as a session holding no files. The NAME of this handler is what the published summary is cut from, and it used to leak: the comment opened \"Files lists …\", which is not this function's identifier, so zipdoc's exact-match strip left it and every SDK, tool list and CLI help line opened with a Go symbol no caller can see. An openapi.Describe stated a better summary beside the route and was DISCARDED — Fold replaces a structural operation with the typed one — so the declaration read as landed and rendered nowhere. The comment is the one home for this sentence. One recursive `find`, the same traversal the artifact sweep makes. It used to be `ls -1A` — top level only — while the sweep collected with `find`, so a run that wrote a nested artifact reported it in its reply and then omitted it here, and the client's prefix match read the file as expired. Two traversals of one directory is two answers about what a session holds; there is one now.
49pub async fn get_exec_files_by_sid(configuration: &configuration::Configuration, sid: &str) -> Result<Vec<models::Listing>, Error<GetExecFilesBySidError>> {
50 // add a prefix to parameters to efficiently prevent name collisions
51 let p_sid = sid;
52
53 let uri_str = format!("{}/v1/exec/files/{sid}", configuration.base_path, sid=crate::apis::urlencode(p_sid));
54 let mut req_builder = configuration.client.request(reqwest::Method::GET, &uri_str);
55
56 if let Some(ref user_agent) = configuration.user_agent {
57 req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
58 }
59 if let Some(ref token) = configuration.bearer_access_token {
60 req_builder = req_builder.bearer_auth(token.to_owned());
61 };
62
63 let req = req_builder.build()?;
64 let resp = configuration.client.execute(req).await?;
65
66 let status = resp.status();
67 let content_type = resp
68 .headers()
69 .get("content-type")
70 .and_then(|v| v.to_str().ok())
71 .unwrap_or("application/octet-stream");
72 let content_type = super::ContentType::from(content_type);
73
74 if !status.is_client_error() && !status.is_server_error() {
75 let content = resp.text().await?;
76 match content_type {
77 ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
78 ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `Vec<models::Listing>`"))),
79 ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `Vec<models::Listing>`")))),
80 }
81 } else {
82 let content = resp.text().await?;
83 let entity: Option<GetExecFilesBySidError> = serde_json::from_str(&content).ok();
84 Err(Error::ResponseError(ResponseContent { status, content, entity }))
85 }
86}
87
88/// Executes a program in a throwaway sandbox and answers with what it printed and what it left behind. `lang` names one of the thirteen the sandbox image carries — py, js, ts, bash, r, php, go, rs, c, cpp, java, d, f90 — and `code` is the whole program, not a fragment: a compiled language is compiled and then run, an interpreted one is interpreted, and `args` becomes the program's own argv either way. Nothing is installed for you; the image is the environment. A PROGRAM THAT FAILS IS A SUCCESSFUL CALL. A non-zero exit answers 200 with the diagnostics on `stderr`, because \"the code threw\" and \"the interpreter is down\" are different facts a caller renders differently. Only the second is an error status. Runs are stateful through `session_id`. Omit it and the run gets a fresh sandbox whose id comes back on the answer; pass that id again and the next run sees the same filesystem, so a program can write a file one call and read it the next. `files` names bytes already uploaded to a session (POST /v1/exec/upload), copied in before the program starts. `files` on the ANSWER is what the program created or changed, by comparison against a marker taken at start — so it is the run's real output, not a listing of the directory — and each is fetched from GET /v1/exec/download/{session}/{name}. The tenant is the caller's, never the body's, at every entry point. A typed op is also an MCP tool and an op-plane op; MCP's tools/call invokes it directly, with no route and therefore no middleware, so nothing there could have checked a credential. tenantOf refuses a context carrying neither a validated principal nor exec's own admission marker, so those entry points fail closed without a second gate to keep in step.
89pub async fn post_exec(configuration: &configuration::Configuration, code_run: models::CodeRun) -> Result<models::CodeResult, Error<PostExecError>> {
90 // add a prefix to parameters to efficiently prevent name collisions
91 let p_code_run = code_run;
92
93 let uri_str = format!("{}/v1/exec", configuration.base_path);
94 let mut req_builder = configuration.client.request(reqwest::Method::POST, &uri_str);
95
96 if let Some(ref user_agent) = configuration.user_agent {
97 req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
98 }
99 if let Some(ref token) = configuration.bearer_access_token {
100 req_builder = req_builder.bearer_auth(token.to_owned());
101 };
102 req_builder = req_builder.json(&p_code_run);
103
104 let req = req_builder.build()?;
105 let resp = configuration.client.execute(req).await?;
106
107 let status = resp.status();
108 let content_type = resp
109 .headers()
110 .get("content-type")
111 .and_then(|v| v.to_str().ok())
112 .unwrap_or("application/octet-stream");
113 let content_type = super::ContentType::from(content_type);
114
115 if !status.is_client_error() && !status.is_server_error() {
116 let content = resp.text().await?;
117 match content_type {
118 ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
119 ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::CodeResult`"))),
120 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::CodeResult`")))),
121 }
122 } else {
123 let content = resp.text().await?;
124 let entity: Option<PostExecError> = serde_json::from_str(&content).ok();
125 Err(Error::ResponseError(ResponseContent { status, content, entity }))
126 }
127}
128
129/// Answers 501 — this deployment does not serve programmatic tool calling. That sentence is the SUMMARY every projection shows, so it says what a caller gets rather than what the code does; the rest names what it would take to stop refusing. /exec/programmatic is NOT this contract's sibling — it is a different protocol on an adjacent path: a multi-round-trip loop where the server suspends a Python program on a tool call, returns the pending calls with a continuation_token, and resumes when the client posts the results back (@hanzochat/agents ProgrammaticToolCalling). Implementing it means implementing suspension and resumption, which is a program, not an endpoint. So it answers 501 with that fact rather than being routed into `run`, which would hand the caller a CodeResult its parser cannot read — a wrong answer, where this is a refusal a client can act on. IT IS A TYPED OP, and the refusal for keeping it raw did not survive reading. It binds no body, opens no stream, relays no other process and sits on no wildcard, so none of the four wire facts that keep a route raw applies to it; what was cited instead was that a permanent stub should declare nothing. That argues for silence in the DOCUMENT and buys the silence everywhere else too — no MCP tool, no CLI command, no SDK method — so a caller could read this address and reach it by no projection but REST, and learn only by calling it that the protocol is not served. Declaring `zip.WithStatus(501)` is what makes typing honest: the document publishes the ONE status this route sends, over an Out with no schema, rather than the 204 a void op would otherwise have invented. ONE delta, pinned by TestProgrammaticRefusesEveryBody: a body that is not JSON now answers 400 rather than 501, because op.invoke decodes before the handler is entered. Both are refusals of a protocol this deployment does not serve, no real caller sends one, and 400 is what the rest of the fleet answers to bytes it cannot parse. It asks tenantOf for the reason every other operation here does, and the answer is the same on the wire it was: over HTTP the credential middleware has already run, so an admitted caller still reads 501. What the call closes is the entry point a route table cannot see — typing an operation makes it an MCP tool, which zip dispatches straight into the handler with no route and therefore no middleware. Uniformity is the whole property: every path into this subsystem reads the admission marker, so there is no operation anybody has to remember is the exception.
130pub async fn post_exec_programmatic(configuration: &configuration::Configuration, ) -> Result<(), Error<PostExecProgrammaticError>> {
131
132 let uri_str = format!("{}/v1/exec/programmatic", configuration.base_path);
133 let mut req_builder = configuration.client.request(reqwest::Method::POST, &uri_str);
134
135 if let Some(ref user_agent) = configuration.user_agent {
136 req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
137 }
138 if let Some(ref token) = configuration.bearer_access_token {
139 req_builder = req_builder.bearer_auth(token.to_owned());
140 };
141
142 let req = req_builder.build()?;
143 let resp = configuration.client.execute(req).await?;
144
145 let status = resp.status();
146
147 if !status.is_client_error() && !status.is_server_error() {
148 Ok(())
149 } else {
150 let content = resp.text().await?;
151 let entity: Option<PostExecProgrammaticError> = serde_json::from_str(&content).ok();
152 Err(Error::ResponseError(ResponseContent { status, content, entity }))
153 }
154}
155
156/// Takes a multipart upload and writes the file into the session's sandbox, so a later run can read it. Answers the session id and the identifier the file is addressed by; `session_id` in the form joins an existing session instead of opening one. The body is multipart/form-data, which is why this is not a typed operation: every non-empty typed body is decoded as JSON.
157pub async fn post_exec_upload(configuration: &configuration::Configuration, ) -> Result<(), Error<PostExecUploadError>> {
158
159 let uri_str = format!("{}/v1/exec/upload", configuration.base_path);
160 let mut req_builder = configuration.client.request(reqwest::Method::POST, &uri_str);
161
162 if let Some(ref user_agent) = configuration.user_agent {
163 req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
164 }
165 if let Some(ref token) = configuration.bearer_access_token {
166 req_builder = req_builder.bearer_auth(token.to_owned());
167 };
168
169 let req = req_builder.build()?;
170 let resp = configuration.client.execute(req).await?;
171
172 let status = resp.status();
173
174 if !status.is_client_error() && !status.is_server_error() {
175 Ok(())
176 } else {
177 let content = resp.text().await?;
178 let entity: Option<PostExecUploadError> = serde_json::from_str(&content).ok();
179 Err(Error::ResponseError(ResponseContent { status, content, entity }))
180 }
181}
182