Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::near::agent::host;
use crate::types::*;

const DOCS_API_BASE: &str = "https://docs.googleapis.com/v1/documents";
const GOOGLE_API_AUTH_REQUIRED_ERROR: &str = "google_api_error_status_401";

/// Make a Google Docs API call.
fn api_call(method: &str, path: &str, body: Option<&str>) -> Result<String, String> {
Expand All @@ -33,11 +34,7 @@ fn api_call(method: &str, path: &str, body: Option<&str>) -> Result<String, Stri
let response = host::http_request(method, &url, headers, body_bytes.as_deref(), None)?;

if response.status < 200 || response.status >= 300 {
let body_text = String::from_utf8_lossy(&response.body);
return Err(format!(
"Google Docs API returned status {}: {}",
response.status, body_text
));
return Err(api_status_error("Google Docs", response.status, &response.body));
}

if response.body.is_empty() {
Expand All @@ -47,6 +44,18 @@ fn api_call(method: &str, path: &str, body: Option<&str>) -> Result<String, Stri
String::from_utf8(response.body).map_err(|e| format!("Invalid UTF-8 in response: {}", e))
}

fn api_status_error(service: &str, status: u16, body: &[u8]) -> String {
if status == 401 {
return serde_json::json!({
"code": GOOGLE_API_AUTH_REQUIRED_ERROR,
"kind": "auth_required",
})
.to_string();
}
let body_text = String::from_utf8_lossy(body);
format!("{service} API returned status {status}: {body_text}")
}

/// Send a batchUpdate to the document and return the parsed response.
fn batch_update_raw(
document_id: &str,
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use crate::types::*;
const DRIVE_API_BASE: &str = "https://www.googleapis.com/drive/v3";
const UPLOAD_API_BASE: &str = "https://www.googleapis.com/upload/drive/v3";
const MAX_DOWNLOAD_TEXT_BYTES: usize = 1_000_000;
const GOOGLE_API_AUTH_REQUIRED_ERROR: &str = "google_api_error_status_401";

/// Standard fields to request for file metadata.
const FILE_FIELDS: &str = "id,name,mimeType,description,size,createdTime,modifiedTime,\
Expand All @@ -36,11 +37,7 @@ fn api_call(method: &str, path: &str, body: Option<&str>) -> Result<String, Stri
let response = host::http_request(method, &url, headers, body_bytes.as_deref(), None)?;

if response.status < 200 || response.status >= 300 {
let body_text = String::from_utf8_lossy(&response.body);
return Err(format!(
"Drive API returned status {}: {}",
response.status, body_text
));
return Err(api_status_error("Drive", response.status, &response.body));
Comment thread
serrrfirat marked this conversation as resolved.
}

if response.body.is_empty() {
Expand All @@ -60,16 +57,24 @@ fn api_call_raw(method: &str, url: &str) -> Result<Vec<u8>, String> {
let response = host::http_request(method, url, "{}", None, None)?;

if response.status < 200 || response.status >= 300 {
let body_text = String::from_utf8_lossy(&response.body);
return Err(format!(
"Drive API returned status {}: {}",
response.status, body_text
));
return Err(api_status_error("Drive", response.status, &response.body));
}

Ok(response.body)
}

fn api_status_error(service: &str, status: u16, body: &[u8]) -> String {
if status == 401 {
return serde_json::json!({
"code": GOOGLE_API_AUTH_REQUIRED_ERROR,
"kind": "auth_required",
})
.to_string();
}
let body_text = String::from_utf8_lossy(body);
format!("{service} API returned status {status}: {body_text}")
}
Comment thread
serrrfirat marked this conversation as resolved.

/// Parse a file resource from the API response.
fn parse_file(v: &serde_json::Value) -> DriveFile {
let mime_type = v["mimeType"].as_str().unwrap_or("").to_string();
Expand Down Expand Up @@ -286,11 +291,7 @@ pub fn upload_file(
let response = host::http_request("POST", &url, &headers, Some(body.as_bytes()), None)?;

if response.status < 200 || response.status >= 300 {
let body_text = String::from_utf8_lossy(&response.body);
return Err(format!(
"Upload failed with status {}: {}",
response.status, body_text
));
return Err(api_status_error("Drive", response.status, &response.body));
}

let parsed: serde_json::Value = serde_json::from_str(
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::near::agent::host;
use crate::types::*;

const SHEETS_API_BASE: &str = "https://sheets.googleapis.com/v4/spreadsheets";
const GOOGLE_API_AUTH_REQUIRED_ERROR: &str = "google_api_error_status_401";

/// Make a Google Sheets API call.
fn api_call(method: &str, path: &str, body: Option<&str>) -> Result<String, String> {
Expand All @@ -33,11 +34,7 @@ fn api_call(method: &str, path: &str, body: Option<&str>) -> Result<String, Stri
let response = host::http_request(method, &url, headers, body_bytes.as_deref(), None)?;

if response.status < 200 || response.status >= 300 {
let body_text = String::from_utf8_lossy(&response.body);
return Err(format!(
"Google Sheets API returned status {}: {}",
response.status, body_text
));
return Err(api_status_error("Google Sheets", response.status, &response.body));
}

if response.body.is_empty() {
Expand All @@ -47,6 +44,18 @@ fn api_call(method: &str, path: &str, body: Option<&str>) -> Result<String, Stri
String::from_utf8(response.body).map_err(|e| format!("Invalid UTF-8 in response: {}", e))
}

fn api_status_error(service: &str, status: u16, body: &[u8]) -> String {
if status == 401 {
return serde_json::json!({
"code": GOOGLE_API_AUTH_REQUIRED_ERROR,
"kind": "auth_required",
})
.to_string();
}
let body_text = String::from_utf8_lossy(body);
format!("{service} API returned status {status}: {body_text}")
}

/// Parse sheet info from the API's JSON.
fn parse_sheet_info(v: &serde_json::Value) -> SheetInfo {
let props = &v["properties"];
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::near::agent::host;
use crate::types::*;

const SLIDES_API_BASE: &str = "https://slides.googleapis.com/v1/presentations";
const GOOGLE_API_AUTH_REQUIRED_ERROR: &str = "google_api_error_status_401";

/// Make a Google Slides API call.
fn api_call(method: &str, path: &str, body: Option<&str>) -> Result<String, String> {
Expand All @@ -33,11 +34,7 @@ fn api_call(method: &str, path: &str, body: Option<&str>) -> Result<String, Stri
let response = host::http_request(method, &url, headers, body_bytes.as_deref(), None)?;

if response.status < 200 || response.status >= 300 {
let body_text = String::from_utf8_lossy(&response.body);
return Err(format!(
"Google Slides API returned status {}: {}",
response.status, body_text
));
return Err(api_status_error("Google Slides", response.status, &response.body));
}

if response.body.is_empty() {
Expand All @@ -47,6 +44,18 @@ fn api_call(method: &str, path: &str, body: Option<&str>) -> Result<String, Stri
String::from_utf8(response.body).map_err(|e| format!("Invalid UTF-8 in response: {}", e))
}

fn api_status_error(service: &str, status: u16, body: &[u8]) -> String {
if status == 401 {
return serde_json::json!({
"code": GOOGLE_API_AUTH_REQUIRED_ERROR,
"kind": "auth_required",
})
.to_string();
}
let body_text = String::from_utf8_lossy(body);
format!("{service} API returned status {status}: {body_text}")
}

/// Send a batchUpdate to the presentation.
fn batch_update_raw(
presentation_id: &str,
Expand Down
Binary file not shown.
128 changes: 127 additions & 1 deletion crates/ironclaw_host_runtime/tests/github_wasm_runtime_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,126 @@ async fn host_runtime_services_routes_google_drive_wasm_list_files_with_scoped_g
);
}

#[tokio::test]
async fn host_runtime_services_maps_google_drive_wasm_401_to_auth_required() {
let capability_id = CapabilityId::new("google-drive.list_files").unwrap();
let scope = sample_scope(InvocationId::new());
let policy = google_drive_policy();
let network = RecordingNetworkHttpEgress::with_status_body(
401,
br#"{"error":{"status":"UNAUTHENTICATED","message":"Invalid Credentials"}}"#.to_vec(),
);
let secret_store = Arc::new(InMemorySecretStore::new());
let account_access_secret = SecretHandle::new("google_drive_access").unwrap();
let required_scopes = vec!["https://www.googleapis.com/auth/drive.readonly".to_string()];
let services = google_wasm_services_for_test!(
"google-drive",
policy.clone(),
network.clone(),
Arc::clone(&secret_store),
account_access_secret.clone(),
required_scopes,
);
secret_store
.put(
scope.clone(),
account_access_secret,
SecretMaterial::from("ya29.expired_fixture_token"),
None,
)
.await
.unwrap();

let outcome = services
.host_runtime_for_local_testing()
.invoke_capability(wasm_runtime_request_for_scope(
capability_id.clone(),
scope,
json!({}),
))
.await
.unwrap();

match outcome {
RuntimeCapabilityOutcome::AuthRequired(gate) => {
assert_eq!(gate.capability_id, capability_id);
assert!(gate.required_secrets.is_empty());
assert!(gate.credential_requirements.is_empty());
}
other => panic!("expected auth-required outcome, got {other:?}"),
}
let requests = network.requests();
assert_eq!(requests.len(), 1);
assert_eq!(requests[0].method, NetworkMethod::Get);
assert!(
requests[0]
.url
.starts_with("https://www.googleapis.com/drive/v3/files?")
);
}

#[tokio::test]
async fn host_runtime_services_maps_google_drive_upload_wasm_401_to_auth_required() {
let capability_id = CapabilityId::new("google-drive.upload_file").unwrap();
let scope = sample_scope(InvocationId::new());
let policy = google_drive_policy();
let network = RecordingNetworkHttpEgress::with_status_body(
401,
br#"{"error":{"status":"UNAUTHENTICATED","message":"Invalid Credentials"}}"#.to_vec(),
);
let secret_store = Arc::new(InMemorySecretStore::new());
let account_access_secret = SecretHandle::new("google_drive_upload_access").unwrap();
let required_scopes = vec!["https://www.googleapis.com/auth/drive".to_string()];
let services = google_wasm_services_for_test!(
"google-drive",
policy.clone(),
network.clone(),
Arc::clone(&secret_store),
account_access_secret.clone(),
required_scopes,
);
secret_store
.put(
scope.clone(),
account_access_secret,
SecretMaterial::from("ya29.expired_upload_fixture_token"),
None,
)
.await
.unwrap();

let outcome = services
.host_runtime_for_local_testing()
.invoke_capability(wasm_runtime_request_for_scope(
capability_id.clone(),
scope,
json!({
"name": "report.txt",
"content": "stale token upload",
"mime_type": "text/plain"
}),
))
.await
.unwrap();

match outcome {
RuntimeCapabilityOutcome::AuthRequired(gate) => {
assert_eq!(gate.capability_id, capability_id);
assert!(gate.required_secrets.is_empty());
assert!(gate.credential_requirements.is_empty());
}
other => panic!("expected auth-required outcome, got {other:?}"),
}
let requests = network.requests();
assert_eq!(requests.len(), 1);
assert_eq!(requests[0].method, NetworkMethod::Post);
assert!(
requests[0]
.url
.starts_with("https://www.googleapis.com/upload/drive/v3/files?")
);
}

#[tokio::test]
async fn host_runtime_services_routes_google_docs_wasm_get_document_with_scoped_google_credential()
{
Expand Down Expand Up @@ -1311,13 +1431,19 @@ fn wasm_error_code_or_text(execution: &WitToolExecution) -> Option<String> {
#[derive(Debug, Clone)]
struct RecordingNetworkHttpEgress {
requests: Arc<std::sync::Mutex<Vec<NetworkHttpRequest>>>,
status: u16,
response_body: Vec<u8>,
}

impl RecordingNetworkHttpEgress {
fn with_body(response_body: Vec<u8>) -> Self {
Self::with_status_body(200, response_body)
}

fn with_status_body(status: u16, response_body: Vec<u8>) -> Self {
Self {
requests: Arc::new(std::sync::Mutex::new(Vec::new())),
status,
response_body,
}
}
Expand All @@ -1336,7 +1462,7 @@ impl NetworkHttpEgress for RecordingNetworkHttpEgress {
let request_bytes = request.body.len() as u64;
self.requests.lock().unwrap().push(request);
Ok(NetworkHttpResponse {
status: 200,
status: self.status,
headers: Vec::new(),
body: self.response_body.clone(),
usage: NetworkUsage {
Expand Down
Loading