Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
9c41b0e
fix(sandbox): send the argv the Cloud Run launcher actually accepts
ItamarZand88 Aug 21, 2026
b901fa4
fix(sandbox): send Azure the image the stack declared
ItamarZand88 Aug 21, 2026
91c7565
fix(sandbox): take the data-plane audience from the package, not the …
ItamarZand88 Aug 21, 2026
c4f53c8
feat(sandbox): move files in and out of an Azure sandbox
ItamarZand88 Aug 21, 2026
95b7e35
fix(sandbox): report an Azure session's real state, and send its vari…
ItamarZand88 Aug 21, 2026
fd8cd6e
feat(sandbox): create Azure sandboxes under the declared egress policy
ItamarZand88 Aug 21, 2026
02b4866
fix(sandbox): refuse a hostname allowlist instead of approximating it
ItamarZand88 Aug 21, 2026
874f5a7
fix(sandbox): fail an Azure create on a permission nobody asked for
ItamarZand88 Aug 21, 2026
f5c403f
feat(sandbox): suspend, resume and auto-suspend an Azure sandbox
ItamarZand88 Aug 21, 2026
07254cd
fix(sandbox): read the state Azure's own auto-suspend produces
ItamarZand88 Aug 21, 2026
f98f1b6
fix(sandbox): close what the pre-push review found
ItamarZand88 Aug 22, 2026
22e1767
docs(sandbox): state what the code does, not what it used to do
ItamarZand88 Aug 22, 2026
52896f3
fix(sandbox): refuse a GCP session id the launcher would read as a flag
ItamarZand88 Aug 22, 2026
2fc9b99
fix(sandbox): close what the confirming review found
ItamarZand88 Aug 22, 2026
ca7f816
fix(sandbox): judge a session's state before its policy, and gate resume
ItamarZand88 Aug 22, 2026
76b8d0b
fix(sandbox): refuse a session that is on its way out, and reap one t…
ItamarZand88 Aug 22, 2026
3377b56
fix(sandbox): judge a session in the state the work will run in
ItamarZand88 Aug 22, 2026
7e3ab83
fix(sandbox): judge a sleeping session before waking it
ItamarZand88 Aug 22, 2026
1555252
fix(sandbox): suspend only the session this call woke
ItamarZand88 Aug 22, 2026
6d4c2ad
fix(sandbox): send the verbs a repeat performs twice exactly once
ItamarZand88 Aug 22, 2026
981b768
fix(sandbox): own a resume whose outcome the data plane never reported
ItamarZand88 Aug 22, 2026
51f27d4
fix(sandbox): keep the caller's variables off the shell that bounds them
ItamarZand88 Aug 22, 2026
ff27c01
docs(sandbox): hold the new comments to the standard
ItamarZand88 Aug 22, 2026
024b408
fix(sandbox): make the wrapper this builds actually run
ItamarZand88 Aug 22, 2026
e4a3de2
fix(sandbox): refuse the loader family, not the two names guessed first
ItamarZand88 Aug 22, 2026
1d0a1c6
fix(sandbox): find the deadline report by its shape, not its position
ItamarZand88 Aug 22, 2026
da08622
docs(sandbox): say what the deadline wrapper does not survive
ItamarZand88 Aug 22, 2026
a1e08ab
docs(sandbox): a refused session is left as it was found
ItamarZand88 Aug 22, 2026
b6179a7
fix(sandbox): send a state transition once, like the verbs beside it
ItamarZand88 Aug 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 69 additions & 66 deletions crates/alien-azure-clients/src/azure/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ impl AzureClientBase {

pub async fn sign_request(
&self,
mut req: http::Request<String>,
mut req: http::Request<Vec<u8>>,
bearer_token: &str,
) -> Result<reqwest::Request> {
// Inject mandatory headers if absent.
Expand Down Expand Up @@ -184,6 +184,55 @@ impl AzureClientBase {

// ------------- Low-level executor -------------

/// Sends a request exactly once, with no retry.
///
/// A repeat PUT to a collection with a server-minted id mints a second resource, and a
/// repeat exec may re-run a command that already started. Neither carries an idempotency key.
pub async fn execute_request_once(
&self,
req: reqwest::Request,
op: &str,
res_name: &str,
) -> Result<reqwest::Response> {
Self::send_once(&self.client, req, op, res_name).await
}

async fn send_once(
client: &reqwest::Client,
req: reqwest::Request,
op: &str,
res_name: &str,
) -> Result<reqwest::Response> {
// Captured before execution consumes the request.
let request_url = req.url().to_string();
let request_body = req.body().and_then(|b| b.as_bytes()).map(|b| {
String::from_utf8_lossy(&b[..b.len().min(MAX_ECHOED_REQUEST_BODY)]).to_string()
});

let resp = client
.execute(req)
.await
.into_alien_error()
.context(ErrorData::HttpRequestFailed {
message: format!("Azure {}: HTTP error for {}", op, res_name),
})?;
let status = resp.status();
if status.is_success() || status == StatusCode::CREATED || status == StatusCode::ACCEPTED {
return Ok(resp);
}

let body = resp.text().await.unwrap_or_default();
Err(create_azure_http_error_with_context(
status,
op,
"Resource",
res_name,
&body,
&request_url,
request_body,
))
}

/// Executes an HTTP request with retry logic and returns the response if successful.
#[cfg(target_arch = "wasm32")]
pub async fn execute_request(
Expand All @@ -207,36 +256,7 @@ impl AzureClientBase {
})
})?;

// Capture request details before execution consumes the request
let request_url = req_clone.url().to_string();
let request_body = req_clone
.body()
.and_then(|b| b.as_bytes())
.map(|b| String::from_utf8_lossy(b).to_string());

let resp = client.execute(req_clone).await.into_alien_error().context(
ErrorData::HttpRequestFailed {
message: format!("Azure {}: HTTP error for {}", op, res_name),
},
)?;
let status = resp.status();
if status.is_success()
|| status == StatusCode::CREATED
|| status == StatusCode::ACCEPTED
{
Ok(resp)
} else {
let body = resp.text().await.unwrap_or_default();
Err(create_azure_http_error_with_context(
status,
&op,
"Resource",
&res_name,
&body,
&request_url,
request_body,
))
}
Self::send_once(&client, req_clone, &op, &res_name).await
}
};
self.with_retry(retryable).await
Expand Down Expand Up @@ -265,36 +285,7 @@ impl AzureClientBase {
})
})?;

// Capture request details before execution consumes the request
let request_url = req_clone.url().to_string();
let request_body = req_clone
.body()
.and_then(|b| b.as_bytes())
.map(|b| String::from_utf8_lossy(b).to_string());

let resp = client.execute(req_clone).await.into_alien_error().context(
ErrorData::HttpRequestFailed {
message: format!("Azure {}: HTTP error for {}", op, res_name),
},
)?;
let status = resp.status();
if status.is_success()
|| status == StatusCode::CREATED
|| status == StatusCode::ACCEPTED
{
Ok(resp)
} else {
let body = resp.text().await.unwrap_or_default();
Err(create_azure_http_error_with_context(
status,
&op,
"Resource",
&res_name,
&body,
&request_url,
request_body,
))
}
Self::send_once(&client, req_clone, &op, &res_name).await
}
};
self.with_retry(retryable).await
Expand Down Expand Up @@ -338,7 +329,7 @@ impl AzureClientBase {
let request_body = req_clone
.body()
.and_then(|b| b.as_bytes())
.map(|b| String::from_utf8_lossy(b).to_string());
.map(|b| String::from_utf8_lossy(&b[..b.len().min(MAX_ECHOED_REQUEST_BODY)]).to_string());

let resp = client.execute(req_clone).await.into_alien_error().context(
ErrorData::HttpRequestFailed {
Expand Down Expand Up @@ -489,7 +480,7 @@ impl AzureClientBase {
let request_body = req_clone
.body()
.and_then(|b| b.as_bytes())
.map(|b| String::from_utf8_lossy(b).to_string());
.map(|b| String::from_utf8_lossy(&b[..b.len().min(MAX_ECHOED_REQUEST_BODY)]).to_string());

let resp = client.execute(req_clone).await.into_alien_error().context(
ErrorData::HttpRequestFailed {
Expand Down Expand Up @@ -606,11 +597,18 @@ impl AzureClientBase {
// Light request-builder (service-agnostic)
// -----------------------------------------------------------------------------

/// How much of a request body is echoed back in an error.
///
/// A failed call quotes the request it sent, and a file upload's body would otherwise become a
/// multi-megabyte error message on its way into a log. Truncated rather than dropped, so a large
/// JSON body still shows the part that usually carries the mistake.
const MAX_ECHOED_REQUEST_BODY: usize = 4096;

pub struct AzureRequestBuilder {
method: Method,
uri: String,
headers: Vec<(String, String)>,
body: String,
body: Vec<u8>,
}

impl AzureRequestBuilder {
Expand All @@ -619,7 +617,7 @@ impl AzureRequestBuilder {
method,
uri,
headers: vec![],
body: String::new(),
body: Vec::new(),
}
}
pub fn header(mut self, name: &str, val: &str) -> Self {
Expand All @@ -639,10 +637,15 @@ impl AzureRequestBuilder {
self.header("content-length", &body.len().to_string())
}
pub fn body(mut self, body: String) -> Self {
self.body = body.into_bytes();
self
}
/// A body that is not text: a file's contents travel as bytes, not as UTF-8.
pub fn body_bytes(mut self, body: Vec<u8>) -> Self {
self.body = body;
self
}
pub fn build(self) -> Result<http::Request<String>> {
pub fn build(self) -> Result<http::Request<Vec<u8>>> {
let mut b = http::Request::builder().method(self.method).uri(&self.uri);
for (k, v) in self.headers {
b = b.header(&k, &v);
Expand Down
Loading
Loading