ai_agent/bridge/
code_session_api.rs1use crate::utils::http::get_user_agent;
11use reqwest::header::{AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue};
12use serde::{Deserialize, Serialize};
13
14const ANTHROPIC_VERSION: &str = "2023-06-01";
15
16fn oauth_headers(access_token: &str) -> HeaderMap {
18 let mut headers = HeaderMap::new();
19 if let Ok(val) = HeaderValue::from_str(&format!("Bearer {}", access_token)) {
20 headers.insert(AUTHORIZATION, val);
21 }
22 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
23 headers.insert(
24 HeaderName::from_static("anthropic-version"),
25 HeaderValue::from_static(ANTHROPIC_VERSION),
26 );
27 headers.insert("User-Agent", get_user_agent().parse().unwrap());
28 headers
29}
30
31pub async fn create_code_session(
42 base_url: &str,
43 access_token: &str,
44 title: &str,
45 timeout_ms: u64,
46 tags: Option<Vec<String>>,
47) -> Option<String> {
48 let url = format!("{}/v1/code/sessions", base_url);
49 let headers = oauth_headers(access_token);
50
51 let mut body = serde_json::json!({
56 "title": title,
57 "bridge": {}
58 });
59
60 if let Some(tags) = tags {
61 if !tags.is_empty() {
62 body["tags"] = serde_json::json!(tags);
63 }
64 }
65
66 let client = reqwest::Client::new();
67 let response = client
68 .post(&url)
69 .headers(headers)
70 .timeout(std::time::Duration::from_millis(timeout_ms))
71 .json(&body)
72 .send()
73 .await
74 .ok()?;
75
76 let status = response.status();
77 if status != reqwest::StatusCode::OK && status != reqwest::StatusCode::CREATED {
78 let status_code = status.as_u16();
79 let body = response.text().await.unwrap_or_default();
80 let detail = extract_error_detail_from_text(&body);
81 log_for_debugging(&format!(
82 "[code-session] Session create failed {}{}",
83 status_code,
84 detail.map(|d| format!(": {}", d)).unwrap_or_default()
85 ));
86 return None;
87 }
88
89 let data: serde_json::Value = response.json().await.ok()?;
90
91 let session = data.get("session")?;
93 let session_obj = session.as_object()?;
94 let id = session_obj.get("id")?.as_str()?;
95 if !id.starts_with("cse_") {
96 let data_str: String = serde_json::to_string(&data)
97 .ok()
98 .map(|s| s.chars().take(200).collect())
99 .unwrap_or_default();
100 log_for_debugging(&format!(
101 "[code-session] No session.id (cse_*) in response: {}",
102 data_str
103 ));
104 return None;
105 }
106
107 Some(id.to_string())
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct RemoteCredentials {
114 #[serde(rename = "worker_jwt")]
115 pub worker_jwt: String,
116 #[serde(rename = "api_base_url")]
117 pub api_base_url: String,
118 #[serde(rename = "expires_in")]
119 pub expires_in: u64,
120 #[serde(rename = "worker_epoch")]
121 pub worker_epoch: i64,
122}
123
124pub async fn fetch_remote_credentials(
135 session_id: &str,
136 base_url: &str,
137 access_token: &str,
138 timeout_ms: u64,
139 trusted_device_token: Option<&str>,
140) -> Option<RemoteCredentials> {
141 let url = format!("{}/v1/code/sessions/{}/bridge", base_url, session_id);
142 let mut headers = oauth_headers(access_token);
143
144 if let Some(token) = trusted_device_token {
145 if let Ok(val) = HeaderValue::from_str(token) {
146 headers.insert(HeaderName::from_static("x-trusted-device-token"), val);
147 }
148 }
149
150 let client = reqwest::Client::new();
151 let response = client
152 .post(&url)
153 .headers(headers)
154 .timeout(std::time::Duration::from_millis(timeout_ms))
155 .json(&serde_json::json!({}))
156 .send()
157 .await
158 .ok()?;
159
160 let status = response.status();
161 if status != reqwest::StatusCode::OK {
162 let status_code = status.as_u16();
163 let body = response.text().await.unwrap_or_default();
164 let detail = extract_error_detail_from_text(&body);
165 log_for_debugging(&format!(
166 "[code-session] /bridge failed {}{}",
167 status_code,
168 detail.map(|d| format!(": {}", d)).unwrap_or_default()
169 ));
170 return None;
171 }
172
173 let data: serde_json::Value = response.json().await.ok()?;
174
175 let worker_jwt = data.get("worker_jwt")?.as_str()?.to_string();
177 let expires_in = data.get("expires_in")?.as_u64()?;
178 let api_base_url = data.get("api_base_url")?.as_str()?.to_string();
179
180 let raw_epoch = data.get("worker_epoch")?;
183 let epoch = if let Some(s) = raw_epoch.as_str() {
184 s.parse().ok()?
185 } else {
186 raw_epoch.as_i64()?
187 };
188
189 Some(RemoteCredentials {
190 worker_jwt,
191 api_base_url,
192 expires_in,
193 worker_epoch: epoch,
194 })
195}
196
197fn extract_error_detail_from_text(body: &str) -> Option<String> {
199 let data: serde_json::Value = serde_json::from_str(body).ok()?;
200 data.get("message")
201 .and_then(|m| m.as_str())
202 .map(|s| s.to_string())
203}
204
205#[allow(unused_variables)]
207fn log_for_debugging(msg: &str) {
208 eprintln!("{}", msg);
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[test]
216 fn test_oauth_headers() {
217 let headers = oauth_headers("test-token");
218 assert!(headers.get("Authorization").is_some());
220 assert!(headers.get("Content-Type").is_some());
222 }
223}