1pub mod actions;
3
4pub use actions::OAuth2ClientProtocol;
5
6use anyhow::{Context, Result};
7use oauth2::{
8 basic::{BasicClient, BasicTokenType},
9 AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, DeviceAuthorizationUrl,
10 EmptyExtraDeviceAuthorizationFields, EmptyExtraTokenFields, PkceCodeChallenge, RedirectUrl,
11 RefreshToken, ResourceOwnerPassword, ResourceOwnerUsername, Scope, StandardTokenResponse,
12 TokenResponse, TokenUrl,
13};
14use std::net::SocketAddr;
15use std::sync::Arc;
16use tokio::sync::mpsc;
17use tracing::{error, info};
18
19use crate::client::http_fetch::FetchClient;
20use crate::client::llm_budget::call_llm_for_client;
21use crate::client::oauth2::actions::{
22 OAUTH2_CLIENT_CONNECTED_EVENT, OAUTH2_DEVICE_CODE_EVENT, OAUTH2_ERROR_EVENT,
23 OAUTH2_TOKEN_OBTAINED_EVENT,
24};
25use crate::llm::actions::client_trait::ClientActionResult;
26use crate::llm::ollama_client::OllamaClient;
27use crate::llm::ClientLlmResult;
28use crate::logging::emit::Log;
29use crate::protocol::Event;
30use crate::state::app_state::AppState;
31use crate::state::client_handles::{ClientCommand, ClientSendOutcome};
32use crate::state::{AccessLogOwner, ClientId as NetGetClientId, ClientStatus};
33
34#[cfg(not(target_arch = "wasm32"))]
37use oauth2::reqwest::async_http_client as http_hook;
38
39#[cfg(target_arch = "wasm32")]
45async fn http_hook(
46 request: oauth2::HttpRequest,
47) -> std::result::Result<oauth2::HttpResponse, crate::client::http_fetch::FetchError> {
48 let client = FetchClient::transport(std::time::Duration::from_secs(30));
49 let headers = request
50 .headers
51 .iter()
52 .map(|(name, value)| (name.as_str().to_string(), value.as_bytes().to_vec()))
53 .collect();
54 let (status, headers, body) = crate::client::http_fetch::round_trip_parts(
55 &client,
56 request.method.as_str(),
57 request.url.as_str(),
58 headers,
59 request.body,
60 )
61 .await?;
62 let mut map = oauth2::http::HeaderMap::new();
63 for (name, value) in headers {
64 if let (Ok(name), Ok(value)) = (
65 oauth2::http::header::HeaderName::from_bytes(name.as_bytes()),
66 oauth2::http::HeaderValue::from_bytes(&value),
67 ) {
68 map.append(name, value);
69 }
70 }
71 let status_code = oauth2::http::StatusCode::from_u16(status).map_err(|e| {
72 crate::client::http_fetch::FetchError(anyhow::anyhow!("status {status}: {e}"))
73 })?;
74 Ok(oauth2::HttpResponse {
75 status_code,
76 headers: map,
77 body: body.to_vec(),
78 })
79}
80
81fn plain_http_client() -> FetchClient {
85 #[cfg(not(target_arch = "wasm32"))]
86 let client = FetchClient::from_reqwest(reqwest::Client::new());
87 #[cfg(target_arch = "wasm32")]
88 let client = FetchClient::transport(std::time::Duration::from_secs(30));
89 client
90}
91
92type TokenResponseType = StandardTokenResponse<EmptyExtraTokenFields, BasicTokenType>;
93
94enum Applied {
102 Ran(String),
104 Disconnect,
106}
107
108#[derive(Clone, Copy)]
110enum Dispatch {
111 Inline,
113 Silent,
120 Deferred,
124}
125
126pub struct OAuth2Client;
128
129impl OAuth2Client {
130 pub async fn connect_with_llm_actions(
132 remote_addr: String,
133 llm_client: OllamaClient,
134 app_state: Arc<AppState>,
135 status_tx: mpsc::UnboundedSender<String>,
136 client_id: NetGetClientId,
137 ) -> Result<SocketAddr> {
138 info!(
139 "OAuth2 client {} initializing for {}",
140 client_id, remote_addr
141 );
142
143 app_state
149 .with_client_mut(client_id, |client| {
150 if let Some(serde_json::Value::Object(params)) = client.startup_params.clone() {
151 for (key, value) in params {
152 if client.get_protocol_field(&key).is_none() {
153 client.set_protocol_field(key, value);
154 }
155 }
156 }
157 })
158 .await;
159
160 let (oauth_client_id, oauth_client_secret, auth_url_opt, token_url, scopes_opt) = app_state
162 .with_client_mut(client_id, |client| {
163 let client_id_val = client
164 .get_protocol_field("client_id")
165 .and_then(|v| v.as_str())
166 .context("Missing OAuth2 client_id startup parameter")?;
167
168 let client_secret_val = client
169 .get_protocol_field("client_secret")
170 .and_then(|v| v.as_str())
171 .map(|s| s.to_string());
172
173 let auth_url = client
174 .get_protocol_field("auth_url")
175 .and_then(|v| v.as_str())
176 .map(|s| s.to_string());
177
178 let token_url_val = client
179 .get_protocol_field("token_url")
180 .and_then(|v| v.as_str())
181 .context("Missing OAuth2 token_url startup parameter")?;
182
183 let scopes = client
184 .get_protocol_field("scopes")
185 .and_then(|v| v.as_str())
186 .map(|s| s.to_string());
187
188 Ok::<_, anyhow::Error>((
189 client_id_val.to_string(),
190 client_secret_val,
191 auth_url,
192 token_url_val.to_string(),
193 scopes,
194 ))
195 })
196 .await
197 .context("Client not found")??;
198
199 crate::client::http_fetch::check_url(&token_url)?;
202
203 let client_id_obj = ClientId::new(oauth_client_id);
205 let client_secret_obj = oauth_client_secret.map(ClientSecret::new);
206
207 let token_url_obj = TokenUrl::new(token_url.clone()).context("Invalid token URL")?;
208
209 let auth_url_obj = if let Some(auth_url_str) = auth_url_opt.clone() {
211 AuthUrl::new(auth_url_str).context("Invalid auth URL")?
212 } else {
213 AuthUrl::new(token_url.clone()).context("Invalid token URL for auth placeholder")?
214 };
215
216 let _oauth_client = BasicClient::new(
217 client_id_obj,
218 client_secret_obj.clone(),
219 auth_url_obj,
220 Some(token_url_obj),
221 );
222
223 app_state
229 .with_client_mut(client_id, |client| {
230 client
231 .set_protocol_field("oauth2_initialized".to_string(), serde_json::json!(true));
232 client.set_protocol_field("token_url".to_string(), serde_json::json!(token_url));
233 if let Some(auth_url) = &auth_url_opt {
234 client.set_protocol_field("auth_url".to_string(), serde_json::json!(auth_url));
235 }
236 if let Some(scopes) = &scopes_opt {
237 client.set_protocol_field(
238 "default_scopes".to_string(),
239 serde_json::json!(scopes),
240 );
241 }
242 })
243 .await;
244
245 app_state
247 .update_client_status(client_id, ClientStatus::Connected)
248 .await;
249 Log::new(Some(&status_tx)).info(format!(
250 "OAuth2 client {} ready for {}",
251 client_id, remote_addr
252 ));
253 let _ = status_tx.send("__UPDATE_UI__".to_string());
254
255 let command_rx =
260 crate::client::command_support::register_command_channel(&app_state, client_id).await;
261 let cmd_task = tokio::spawn(Self::command_loop(
262 command_rx,
263 client_id,
264 app_state.clone(),
265 llm_client.clone(),
266 status_tx.clone(),
267 ));
268 app_state.register_client_task(client_id, cmd_task).await;
269
270 if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
272 let protocol = Arc::new(OAuth2ClientProtocol::new());
273 let event = Event::new(
274 &OAUTH2_CLIENT_CONNECTED_EVENT,
275 serde_json::json!({
276 "token_url": token_url,
277 "auth_url": auth_url_opt,
278 }),
279 );
280
281 let memory = app_state
282 .get_memory_for_client(client_id)
283 .await
284 .unwrap_or_default();
285
286 match call_llm_for_client(
287 &llm_client,
288 &app_state,
289 client_id.to_string(),
290 &instruction,
291 &memory,
292 Some(&event),
293 protocol.as_ref(),
294 &status_tx,
295 )
296 .await
297 {
298 Ok(ClientLlmResult {
299 actions,
300 memory_updates,
301 }) => {
302 if let Some(mem) = memory_updates {
304 app_state.set_memory_for_client(client_id, mem).await;
305 }
306
307 for action in actions {
309 use crate::llm::actions::client_trait::Client;
310 let result = match protocol.execute_action(action) {
311 Ok(result) => result,
312 Err(e) => {
313 error!("OAuth2 client {} rejected action: {}", client_id, e);
314 continue;
315 }
316 };
317 match Self::apply_action(
318 result,
319 Dispatch::Inline,
320 client_id,
321 app_state.clone(),
322 llm_client.clone(),
323 status_tx.clone(),
324 )
325 .await
326 {
327 Ok(Applied::Ran(detail)) => {
328 info!("OAuth2 client {}: {}", client_id, detail)
329 }
330 Ok(Applied::Disconnect) => break,
331 Err(e) => error!("Failed to execute OAuth2 action: {}", e),
332 }
333 }
334 }
335 Err(e) => {
336 error!("LLM error for OAuth2 client {}: {}", client_id, e);
337 }
338 }
339 }
340
341 Ok("0.0.0.0:0".parse().unwrap())
347 }
348
349 async fn apply_action(
357 action_result: ClientActionResult,
358 dispatch: Dispatch,
359 client_id: NetGetClientId,
360 app_state: Arc<AppState>,
361 llm_client: OllamaClient,
362 status_tx: mpsc::UnboundedSender<String>,
363 ) -> Result<Applied> {
364 match action_result {
365 ClientActionResult::Custom { name, data } => match name.as_str() {
366 "oauth2_exchange_password" => {
367 Self::exchange_password(
368 client_id,
369 data,
370 app_state.clone(),
371 llm_client,
372 status_tx,
373 dispatch,
374 )
375 .await?;
376 Ok(Applied::Ran(
377 Self::token_state_detail(&name, client_id, &app_state).await,
378 ))
379 }
380 "oauth2_exchange_client_credentials" => {
381 Self::exchange_client_credentials(
382 client_id,
383 data,
384 app_state.clone(),
385 llm_client,
386 status_tx,
387 dispatch,
388 )
389 .await?;
390 Ok(Applied::Ran(
391 Self::token_state_detail(&name, client_id, &app_state).await,
392 ))
393 }
394 "oauth2_start_device_code" => {
395 Self::start_device_code_flow(
396 client_id,
397 data,
398 app_state.clone(),
399 llm_client,
400 status_tx,
401 dispatch,
402 )
403 .await?;
404 let has_code = app_state
405 .with_client_mut(client_id, |client| {
406 client.get_protocol_field("device_code").is_some()
407 })
408 .await
409 .unwrap_or(false);
410 Ok(Applied::Ran(if has_code {
411 "oauth2_start_device_code: device code issued, polling started".to_string()
412 } else {
413 "oauth2_start_device_code: provider issued no device code".to_string()
414 }))
415 }
416 "oauth2_poll_device_code" => {
417 Self::poll_device_code(
418 client_id,
419 app_state.clone(),
420 llm_client,
421 status_tx,
422 dispatch,
423 )
424 .await?;
425 Ok(Applied::Ran(
426 Self::token_state_detail(&name, client_id, &app_state).await,
427 ))
428 }
429 "oauth2_refresh_token" => {
430 Self::refresh_token(
431 client_id,
432 app_state.clone(),
433 llm_client,
434 status_tx,
435 dispatch,
436 )
437 .await?;
438 Ok(Applied::Ran(
439 Self::token_state_detail(&name, client_id, &app_state).await,
440 ))
441 }
442 "oauth2_generate_auth_url" => {
443 Self::generate_auth_url(client_id, data, app_state, status_tx).await?;
444 Ok(Applied::Ran(
445 "oauth2_generate_auth_url: authorization URL built (nothing sent; the \
446 user's browser carries it)"
447 .to_string(),
448 ))
449 }
450 "oauth2_exchange_code" => {
451 Self::exchange_code(
452 client_id,
453 data,
454 app_state.clone(),
455 llm_client,
456 status_tx,
457 dispatch,
458 )
459 .await?;
460 Ok(Applied::Ran(
461 Self::token_state_detail(&name, client_id, &app_state).await,
462 ))
463 }
464 other => Err(anyhow::anyhow!(
465 "OAuth2 client cannot execute custom result '{}'",
466 other
467 )),
468 },
469 ClientActionResult::Disconnect => {
470 info!("OAuth2 client {} disconnecting", client_id);
471 app_state
472 .update_client_status(client_id, ClientStatus::Disconnected)
473 .await;
474 let _ = status_tx.send("__UPDATE_UI__".to_string());
475 Ok(Applied::Disconnect)
476 }
477 _ => Ok(Applied::Ran(
480 "no OAuth2 flow run (action produced no request)".to_string(),
481 )),
482 }
483 }
484
485 async fn token_state_detail(
487 name: &str,
488 client_id: NetGetClientId,
489 app_state: &Arc<AppState>,
490 ) -> String {
491 let has_token = app_state
492 .with_client_mut(client_id, |client| {
493 client.get_protocol_field("access_token").is_some()
494 })
495 .await
496 .unwrap_or(false);
497 if has_token {
498 format!("{name}: completed, an access token is stored")
499 } else {
500 format!(
501 "{name}: completed but no access token was stored - the provider did not \
502 issue one (see the oauth2_error event and netget.log)"
503 )
504 }
505 }
506
507 fn build_oauth_client(
509 oauth_client_id: String,
510 oauth_client_secret: Option<String>,
511 auth_url_opt: Option<String>,
512 token_url: String,
513 device_auth_url_opt: Option<String>,
514 ) -> Result<BasicClient> {
515 let client_id_obj = ClientId::new(oauth_client_id);
516 let client_secret_obj = oauth_client_secret.map(ClientSecret::new);
517 let token_url_obj = TokenUrl::new(token_url.clone())?;
518
519 let auth_url_obj = if let Some(auth_url_str) = auth_url_opt {
521 AuthUrl::new(auth_url_str)?
522 } else {
523 AuthUrl::new(token_url)?
524 };
525
526 let mut oauth_client = BasicClient::new(
527 client_id_obj,
528 client_secret_obj,
529 auth_url_obj,
530 Some(token_url_obj),
531 );
532
533 if let Some(device_url_str) = device_auth_url_opt {
534 oauth_client = oauth_client
535 .set_device_authorization_url(DeviceAuthorizationUrl::new(device_url_str)?);
536 }
537
538 Ok(oauth_client)
539 }
540
541 async fn exchange_password(
543 client_id: NetGetClientId,
544 data: serde_json::Value,
545 app_state: Arc<AppState>,
546 llm_client: OllamaClient,
547 status_tx: mpsc::UnboundedSender<String>,
548 dispatch: Dispatch,
549 ) -> Result<()> {
550 let username = data["username"]
551 .as_str()
552 .context("Missing username")?
553 .to_string();
554 let password = data["password"]
555 .as_str()
556 .context("Missing password")?
557 .to_string();
558 let scopes_str = data["scopes"].as_str().map(|s| s.to_string());
559
560 info!("OAuth2 client {} exchanging password for token", client_id);
561
562 let (oauth_client_id, oauth_client_secret, auth_url, token_url, _) = app_state
564 .with_client_mut(client_id, |client| {
565 let cid = client
566 .get_protocol_field("client_id")
567 .and_then(|v| v.as_str())
568 .map(|s| s.to_string())
569 .context("Missing client_id")?;
570 let csecret = client
571 .get_protocol_field("client_secret")
572 .and_then(|v| v.as_str())
573 .map(|s| s.to_string());
574 let aurl = client
575 .get_protocol_field("auth_url")
576 .and_then(|v| v.as_str())
577 .map(|s| s.to_string());
578 let turl = client
579 .get_protocol_field("token_url")
580 .and_then(|v| v.as_str())
581 .map(|s| s.to_string())
582 .context("Missing token_url")?;
583 let dscopes = client
584 .get_protocol_field("default_scopes")
585 .and_then(|v| v.as_str())
586 .map(|s| s.to_string());
587 Ok::<_, anyhow::Error>((cid, csecret, aurl, turl, dscopes))
588 })
589 .await
590 .context("Client not found")??;
591
592 let oauth_client = Self::build_oauth_client(
593 oauth_client_id,
594 oauth_client_secret,
595 auth_url,
596 token_url,
597 None,
598 )?;
599
600 let username_obj = ResourceOwnerUsername::new(username);
602 let password_obj = ResourceOwnerPassword::new(password);
603 let mut token_request = oauth_client.exchange_password(&username_obj, &password_obj);
604
605 if let Some(scopes) = scopes_str {
607 for scope in scopes.split_whitespace() {
608 token_request = token_request.add_scope(Scope::new(scope.to_string()));
609 }
610 }
611
612 match token_request.request_async(http_hook).await {
614 Ok(token_response) => {
615 Self::handle_token_response(
616 client_id,
617 token_response,
618 app_state,
619 llm_client,
620 status_tx,
621 dispatch,
622 )
623 .await?;
624 }
625 Err(e) => {
626 Self::handle_oauth_error(
627 client_id,
628 format!("password_exchange_failed: {}", e),
629 app_state,
630 llm_client,
631 status_tx,
632 dispatch,
633 )
634 .await?;
635 }
636 }
637
638 Ok(())
639 }
640
641 async fn exchange_client_credentials(
643 client_id: NetGetClientId,
644 data: serde_json::Value,
645 app_state: Arc<AppState>,
646 llm_client: OllamaClient,
647 status_tx: mpsc::UnboundedSender<String>,
648 dispatch: Dispatch,
649 ) -> Result<()> {
650 let scopes_str = data["scopes"].as_str().map(|s| s.to_string());
651
652 info!(
653 "OAuth2 client {} exchanging client credentials for token",
654 client_id
655 );
656
657 let (oauth_client_id, oauth_client_secret, auth_url, token_url, _) = app_state
659 .with_client_mut(client_id, |client| {
660 let cid = client
661 .get_protocol_field("client_id")
662 .and_then(|v| v.as_str())
663 .map(|s| s.to_string())
664 .context("Missing client_id")?;
665 let csecret = client
666 .get_protocol_field("client_secret")
667 .and_then(|v| v.as_str())
668 .map(|s| s.to_string());
669 let aurl = client
670 .get_protocol_field("auth_url")
671 .and_then(|v| v.as_str())
672 .map(|s| s.to_string());
673 let turl = client
674 .get_protocol_field("token_url")
675 .and_then(|v| v.as_str())
676 .map(|s| s.to_string())
677 .context("Missing token_url")?;
678 let dscopes = client
679 .get_protocol_field("default_scopes")
680 .and_then(|v| v.as_str())
681 .map(|s| s.to_string());
682 Ok::<_, anyhow::Error>((cid, csecret, aurl, turl, dscopes))
683 })
684 .await
685 .context("Client not found")??;
686
687 let oauth_client = Self::build_oauth_client(
688 oauth_client_id,
689 oauth_client_secret,
690 auth_url,
691 token_url,
692 None,
693 )?;
694
695 let mut token_request = oauth_client.exchange_client_credentials();
697
698 if let Some(scopes) = scopes_str {
700 for scope in scopes.split_whitespace() {
701 token_request = token_request.add_scope(Scope::new(scope.to_string()));
702 }
703 }
704
705 match token_request.request_async(http_hook).await {
707 Ok(token_response) => {
708 Self::handle_token_response(
709 client_id,
710 token_response,
711 app_state,
712 llm_client,
713 status_tx,
714 dispatch,
715 )
716 .await?;
717 }
718 Err(e) => {
719 Self::handle_oauth_error(
720 client_id,
721 format!("client_credentials_exchange_failed: {}", e),
722 app_state,
723 llm_client,
724 status_tx,
725 dispatch,
726 )
727 .await?;
728 }
729 }
730
731 Ok(())
732 }
733
734 async fn start_device_code_flow(
736 client_id: NetGetClientId,
737 data: serde_json::Value,
738 app_state: Arc<AppState>,
739 llm_client: OllamaClient,
740 status_tx: mpsc::UnboundedSender<String>,
741 dispatch: Dispatch,
742 ) -> Result<()> {
743 let scopes_str = data["scopes"].as_str().map(|s| s.to_string());
744
745 info!("OAuth2 client {} starting device code flow", client_id);
746
747 let (oauth_client_id, oauth_client_secret, auth_url, token_url, device_auth_url) =
749 app_state
750 .with_client_mut(client_id, |client| {
751 let cid = client
752 .get_protocol_field("client_id")
753 .and_then(|v| v.as_str())
754 .map(|s| s.to_string())
755 .context("Missing client_id")?;
756 let csecret = client
757 .get_protocol_field("client_secret")
758 .and_then(|v| v.as_str())
759 .map(|s| s.to_string());
760 let aurl = client
761 .get_protocol_field("auth_url")
762 .and_then(|v| v.as_str())
763 .map(|s| s.to_string());
764 let turl = client
765 .get_protocol_field("token_url")
766 .and_then(|v| v.as_str())
767 .map(|s| s.to_string())
768 .context("Missing token_url")?;
769 let durl = client
770 .get_protocol_field("device_auth_url")
771 .and_then(|v| v.as_str())
772 .map(|s| s.to_string());
773 Ok::<_, anyhow::Error>((cid, csecret, aurl, turl, durl))
774 })
775 .await
776 .context("Client not found")??;
777
778 let oauth_client = Self::build_oauth_client(
779 oauth_client_id,
780 oauth_client_secret,
781 auth_url,
782 token_url,
783 device_auth_url,
784 )?;
785
786 let mut device_auth_request = oauth_client
788 .exchange_device_code()
789 .context("Device authorization URL not configured")?;
790
791 if let Some(scopes) = scopes_str {
793 for scope in scopes.split_whitespace() {
794 device_auth_request = device_auth_request.add_scope(Scope::new(scope.to_string()));
795 }
796 }
797
798 match device_auth_request
800 .request_async::<_, _, _, EmptyExtraDeviceAuthorizationFields>(http_hook)
801 .await
802 {
803 Ok(device_response) => {
804 let verification_uri = device_response.verification_uri().to_string();
805 let user_code = device_response.user_code().secret().to_string();
806 let device_code = device_response.device_code().secret().to_string();
807 let interval = device_response.interval().as_secs();
808
809 info!(
810 "OAuth2 client {} device code flow: visit {} and enter code {}",
811 client_id, verification_uri, user_code
812 );
813
814 app_state
816 .with_client_mut(client_id, |client| {
817 client.set_protocol_field(
818 "device_code".to_string(),
819 serde_json::json!(device_code),
820 );
821 client.set_protocol_field(
822 "polling_interval".to_string(),
823 serde_json::json!(interval),
824 );
825 })
826 .await;
827
828 let event = Event::new(
830 &OAUTH2_DEVICE_CODE_EVENT,
831 serde_json::json!({
832 "verification_uri": verification_uri,
833 "user_code": user_code,
834 "device_code": "[REDACTED]",
835 "interval": interval,
836 }),
837 );
838 Self::notify_event(
839 client_id,
840 event,
841 app_state.clone(),
842 llm_client.clone(),
843 status_tx.clone(),
844 dispatch,
845 )
846 .await;
847
848 let app_state_clone = app_state.clone();
850 let llm_client_clone = llm_client.clone();
851 let status_tx_clone = status_tx.clone();
852 let task_owner = app_state.clone();
854 task_owner
855 .spawn_client_task(client_id, async move {
856 for _ in 0..60 {
857 tokio::time::sleep(std::time::Duration::from_secs(interval)).await;
859
860 if let Err(e) = Self::poll_device_code(
861 client_id,
862 app_state_clone.clone(),
863 llm_client_clone.clone(),
864 status_tx_clone.clone(),
865 Dispatch::Inline,
868 )
869 .await
870 {
871 error!("Device code polling error: {}", e);
872 break;
873 }
874
875 let has_token = app_state_clone
877 .with_client_mut(client_id, |client| {
878 client.get_protocol_field("access_token").is_some()
879 })
880 .await
881 .unwrap_or(false);
882
883 if has_token {
884 break;
885 }
886 }
887 })
888 .await;
889 }
890 Err(e) => {
891 Self::handle_oauth_error(
892 client_id,
893 format!("device_code_failed: {}", e),
894 app_state,
895 llm_client,
896 status_tx,
897 dispatch,
898 )
899 .await?;
900 }
901 }
902
903 Ok(())
904 }
905
906 async fn poll_device_code(
908 client_id: NetGetClientId,
909 app_state: Arc<AppState>,
910 llm_client: OllamaClient,
911 status_tx: mpsc::UnboundedSender<String>,
912 dispatch: Dispatch,
913 ) -> Result<()> {
914 let (device_code_str, oauth_client_id, oauth_client_secret, _auth_url, token_url) =
916 app_state
917 .with_client_mut(client_id, |client| {
918 let dc = client
919 .get_protocol_field("device_code")
920 .and_then(|v| v.as_str())
921 .map(|s| s.to_string())
922 .context("Missing device_code")?;
923 let cid = client
924 .get_protocol_field("client_id")
925 .and_then(|v| v.as_str())
926 .map(|s| s.to_string())
927 .context("Missing client_id")?;
928 let csecret = client
929 .get_protocol_field("client_secret")
930 .and_then(|v| v.as_str())
931 .map(|s| s.to_string());
932 let aurl = client
933 .get_protocol_field("auth_url")
934 .and_then(|v| v.as_str())
935 .map(|s| s.to_string());
936 let turl = client
937 .get_protocol_field("token_url")
938 .and_then(|v| v.as_str())
939 .map(|s| s.to_string())
940 .context("Missing token_url")?;
941 Ok::<_, anyhow::Error>((dc, cid, csecret, aurl, turl))
942 })
943 .await
944 .context("Client not found")??;
945
946 let client = plain_http_client();
949 let mut params = vec![
950 ("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
951 ("device_code", device_code_str.as_str()),
952 ("client_id", oauth_client_id.as_str()),
953 ];
954
955 let client_secret_param;
957 if let Some(ref secret) = oauth_client_secret {
958 client_secret_param = secret.clone();
959 params.push(("client_secret", client_secret_param.as_str()));
960 }
961
962 match client.post(&token_url).form(¶ms).send().await {
963 Ok(response) => {
964 let status = response.status();
965 let body: serde_json::Value = response.json().await?;
966
967 if status.is_success() {
968 if let Some(access_token) = body.get("access_token").and_then(|v| v.as_str()) {
970 let token_type = body
971 .get("token_type")
972 .and_then(|v| v.as_str())
973 .unwrap_or("Bearer");
974 let expires_in = body
975 .get("expires_in")
976 .and_then(|v| v.as_u64())
977 .unwrap_or(3600);
978 let refresh_token = body
979 .get("refresh_token")
980 .and_then(|v| v.as_str())
981 .map(|s| s.to_string());
982 let scope = body.get("scope").and_then(|v| v.as_str()).unwrap_or("");
983
984 app_state
986 .with_client_mut(client_id, |client| {
987 client.set_protocol_field(
988 "access_token".to_string(),
989 serde_json::json!(access_token),
990 );
991 client.set_protocol_field(
992 "token_type".to_string(),
993 serde_json::json!(token_type),
994 );
995 client.set_protocol_field(
996 "expires_in".to_string(),
997 serde_json::json!(expires_in),
998 );
999 if let Some(rt) = &refresh_token {
1000 client.set_protocol_field(
1001 "refresh_token".to_string(),
1002 serde_json::json!(rt),
1003 );
1004 }
1005 if !scope.is_empty() {
1006 client.set_protocol_field(
1007 "scopes".to_string(),
1008 serde_json::json!(scope),
1009 );
1010 }
1011 })
1012 .await;
1013
1014 info!("OAuth2 client {} device code token obtained", client_id);
1015
1016 let event = Event::new(
1018 &OAUTH2_TOKEN_OBTAINED_EVENT,
1019 serde_json::json!({
1020 "access_token": "[REDACTED]",
1021 "token_type": token_type,
1022 "expires_in": expires_in,
1023 "refresh_token": if refresh_token.is_some() { "[REDACTED]" } else { "" },
1024 "scope": scope,
1025 }),
1026 );
1027 Self::notify_event(
1028 client_id,
1029 event,
1030 app_state.clone(),
1031 llm_client.clone(),
1032 status_tx.clone(),
1033 dispatch,
1034 )
1035 .await;
1036 }
1037 } else {
1038 let error = body
1040 .get("error")
1041 .and_then(|v| v.as_str())
1042 .unwrap_or("unknown");
1043 if error != "authorization_pending" && error != "slow_down" {
1044 error!("Device code polling error: {}", error);
1045 }
1046 }
1047 }
1048 Err(e) => {
1049 error!("Device code polling HTTP error: {}", e);
1050 }
1051 }
1052
1053 Ok(())
1054 }
1055
1056 async fn refresh_token(
1058 client_id: NetGetClientId,
1059 app_state: Arc<AppState>,
1060 llm_client: OllamaClient,
1061 status_tx: mpsc::UnboundedSender<String>,
1062 dispatch: Dispatch,
1063 ) -> Result<()> {
1064 info!("OAuth2 client {} refreshing token", client_id);
1065
1066 let (refresh_token_str, oauth_client_id, oauth_client_secret, auth_url, token_url) =
1068 app_state
1069 .with_client_mut(client_id, |client| {
1070 let rt = client
1071 .get_protocol_field("refresh_token")
1072 .and_then(|v| v.as_str())
1073 .map(|s| s.to_string())
1074 .context("No refresh token available")?;
1075 let cid = client
1076 .get_protocol_field("client_id")
1077 .and_then(|v| v.as_str())
1078 .map(|s| s.to_string())
1079 .context("Missing client_id")?;
1080 let csecret = client
1081 .get_protocol_field("client_secret")
1082 .and_then(|v| v.as_str())
1083 .map(|s| s.to_string());
1084 let aurl = client
1085 .get_protocol_field("auth_url")
1086 .and_then(|v| v.as_str())
1087 .map(|s| s.to_string());
1088 let turl = client
1089 .get_protocol_field("token_url")
1090 .and_then(|v| v.as_str())
1091 .map(|s| s.to_string())
1092 .context("Missing token_url")?;
1093 Ok::<_, anyhow::Error>((rt, cid, csecret, aurl, turl))
1094 })
1095 .await
1096 .context("Client not found")??;
1097
1098 let oauth_client = Self::build_oauth_client(
1099 oauth_client_id,
1100 oauth_client_secret,
1101 auth_url,
1102 token_url,
1103 None,
1104 )?;
1105
1106 match oauth_client
1108 .exchange_refresh_token(&RefreshToken::new(refresh_token_str))
1109 .request_async(http_hook)
1110 .await
1111 {
1112 Ok(token_response) => {
1113 Self::handle_token_response(
1114 client_id,
1115 token_response,
1116 app_state,
1117 llm_client,
1118 status_tx,
1119 dispatch,
1120 )
1121 .await?;
1122 }
1123 Err(e) => {
1124 Self::handle_oauth_error(
1125 client_id,
1126 format!("token_refresh_failed: {}", e),
1127 app_state,
1128 llm_client,
1129 status_tx,
1130 dispatch,
1131 )
1132 .await?;
1133 }
1134 }
1135
1136 Ok(())
1137 }
1138
1139 async fn generate_auth_url(
1141 client_id: NetGetClientId,
1142 data: serde_json::Value,
1143 app_state: Arc<AppState>,
1144 status_tx: mpsc::UnboundedSender<String>,
1145 ) -> Result<()> {
1146 let scopes_str = data["scopes"].as_str().map(|s| s.to_string());
1147 let redirect_uri_str = data["redirect_uri"]
1148 .as_str()
1149 .unwrap_or("http://localhost:8080/callback");
1150
1151 info!("OAuth2 client {} generating auth URL", client_id);
1152
1153 let (oauth_client_id, oauth_client_secret, auth_url, token_url, _) = app_state
1155 .with_client_mut(client_id, |client| {
1156 let cid = client
1157 .get_protocol_field("client_id")
1158 .and_then(|v| v.as_str())
1159 .map(|s| s.to_string())
1160 .context("Missing client_id")?;
1161 let csecret = client
1162 .get_protocol_field("client_secret")
1163 .and_then(|v| v.as_str())
1164 .map(|s| s.to_string());
1165 let aurl = client
1166 .get_protocol_field("auth_url")
1167 .and_then(|v| v.as_str())
1168 .map(|s| s.to_string())
1169 .context("Missing auth_url for authorization code flow")?;
1170 let turl = client
1171 .get_protocol_field("token_url")
1172 .and_then(|v| v.as_str())
1173 .map(|s| s.to_string())
1174 .context("Missing token_url")?;
1175 let dscopes = client
1176 .get_protocol_field("default_scopes")
1177 .and_then(|v| v.as_str())
1178 .map(|s| s.to_string());
1179 Ok::<_, anyhow::Error>((cid, csecret, Some(aurl), turl, dscopes))
1180 })
1181 .await
1182 .context("Client not found")??;
1183
1184 let mut oauth_client = Self::build_oauth_client(
1185 oauth_client_id,
1186 oauth_client_secret,
1187 auth_url,
1188 token_url,
1189 None,
1190 )?;
1191
1192 oauth_client =
1194 oauth_client.set_redirect_uri(RedirectUrl::new(redirect_uri_str.to_string())?);
1195
1196 let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
1198
1199 let mut auth_request = oauth_client
1201 .authorize_url(CsrfToken::new_random)
1202 .set_pkce_challenge(pkce_challenge);
1203
1204 if let Some(scopes) = scopes_str {
1206 for scope in scopes.split_whitespace() {
1207 auth_request = auth_request.add_scope(Scope::new(scope.to_string()));
1208 }
1209 }
1210
1211 let (auth_url_result, csrf_token) = auth_request.url();
1212
1213 app_state
1215 .with_client_mut(client_id, |client| {
1216 client.set_protocol_field(
1217 "pkce_verifier".to_string(),
1218 serde_json::json!(pkce_verifier.secret()),
1219 );
1220 client.set_protocol_field(
1221 "csrf_token".to_string(),
1222 serde_json::json!(csrf_token.secret()),
1223 );
1224 client.set_protocol_field(
1225 "redirect_uri".to_string(),
1226 serde_json::json!(redirect_uri_str),
1227 );
1228 })
1229 .await;
1230
1231 let log = Log::new(Some(&status_tx));
1232 log.info(format!("OAuth2 authorization URL: {}", auth_url_result));
1233 log.info("Visit the URL above to authorize, then paste the code".to_string());
1234
1235 Ok(())
1236 }
1237
1238 async fn exchange_code(
1240 client_id: NetGetClientId,
1241 data: serde_json::Value,
1242 app_state: Arc<AppState>,
1243 llm_client: OllamaClient,
1244 status_tx: mpsc::UnboundedSender<String>,
1245 dispatch: Dispatch,
1246 ) -> Result<()> {
1247 let code = data["code"].as_str().context("Missing code")?.to_string();
1248
1249 info!("OAuth2 client {} exchanging authorization code", client_id);
1250
1251 let (
1253 pkce_verifier_str,
1254 redirect_uri,
1255 oauth_client_id,
1256 oauth_client_secret,
1257 auth_url,
1258 token_url,
1259 ) = app_state
1260 .with_client_mut(client_id, |client| {
1261 let pkce = client
1262 .get_protocol_field("pkce_verifier")
1263 .and_then(|v| v.as_str())
1264 .map(|s| s.to_string())
1265 .context("Missing PKCE verifier")?;
1266 let redir = client
1267 .get_protocol_field("redirect_uri")
1268 .and_then(|v| v.as_str())
1269 .map(|s| s.to_string())
1270 .context("Missing redirect_uri")?;
1271 let cid = client
1272 .get_protocol_field("client_id")
1273 .and_then(|v| v.as_str())
1274 .map(|s| s.to_string())
1275 .context("Missing client_id")?;
1276 let csecret = client
1277 .get_protocol_field("client_secret")
1278 .and_then(|v| v.as_str())
1279 .map(|s| s.to_string());
1280 let aurl = client
1281 .get_protocol_field("auth_url")
1282 .and_then(|v| v.as_str())
1283 .map(|s| s.to_string());
1284 let turl = client
1285 .get_protocol_field("token_url")
1286 .and_then(|v| v.as_str())
1287 .map(|s| s.to_string())
1288 .context("Missing token_url")?;
1289 Ok::<_, anyhow::Error>((pkce, redir, cid, csecret, aurl, turl))
1290 })
1291 .await
1292 .context("Client not found")??;
1293
1294 let mut oauth_client = Self::build_oauth_client(
1295 oauth_client_id,
1296 oauth_client_secret,
1297 auth_url,
1298 token_url,
1299 None,
1300 )?;
1301
1302 oauth_client = oauth_client.set_redirect_uri(RedirectUrl::new(redirect_uri)?);
1303
1304 let token_request = oauth_client
1306 .exchange_code(AuthorizationCode::new(code))
1307 .set_pkce_verifier(oauth2::PkceCodeVerifier::new(pkce_verifier_str));
1308
1309 match token_request.request_async(http_hook).await {
1310 Ok(token_response) => {
1311 Self::handle_token_response(
1312 client_id,
1313 token_response,
1314 app_state,
1315 llm_client,
1316 status_tx,
1317 dispatch,
1318 )
1319 .await?;
1320 }
1321 Err(e) => {
1322 Self::handle_oauth_error(
1323 client_id,
1324 format!("code_exchange_failed: {}", e),
1325 app_state,
1326 llm_client,
1327 status_tx,
1328 dispatch,
1329 )
1330 .await?;
1331 }
1332 }
1333
1334 Ok(())
1335 }
1336
1337 async fn handle_token_response(
1339 client_id: NetGetClientId,
1340 token_response: TokenResponseType,
1341 app_state: Arc<AppState>,
1342 llm_client: OllamaClient,
1343 status_tx: mpsc::UnboundedSender<String>,
1344 dispatch: Dispatch,
1345 ) -> Result<()> {
1346 let access_token = token_response.access_token().secret().to_string();
1347 let token_type = token_response.token_type().as_ref().to_string();
1348 let expires_in = token_response
1349 .expires_in()
1350 .map(|d| d.as_secs())
1351 .unwrap_or(0);
1352 let refresh_token = token_response
1353 .refresh_token()
1354 .map(|rt| rt.secret().to_string());
1355 let scopes = token_response
1356 .scopes()
1357 .map(|s| {
1358 s.iter()
1359 .map(|scope| scope.to_string())
1360 .collect::<Vec<_>>()
1361 .join(" ")
1362 })
1363 .unwrap_or_default();
1364
1365 info!(
1366 "OAuth2 client {} obtained access token (expires in {}s)",
1367 client_id, expires_in
1368 );
1369
1370 app_state
1372 .with_client_mut(client_id, |client| {
1373 client.set_protocol_field(
1374 "access_token".to_string(),
1375 serde_json::json!(access_token),
1376 );
1377 client.set_protocol_field("token_type".to_string(), serde_json::json!(token_type));
1378 client.set_protocol_field("expires_in".to_string(), serde_json::json!(expires_in));
1379 if let Some(rt) = &refresh_token {
1380 client.set_protocol_field("refresh_token".to_string(), serde_json::json!(rt));
1381 }
1382 if !scopes.is_empty() {
1383 client.set_protocol_field("scopes".to_string(), serde_json::json!(scopes));
1384 }
1385 })
1386 .await;
1387
1388 let event = Event::new(
1390 &OAUTH2_TOKEN_OBTAINED_EVENT,
1391 serde_json::json!({
1392 "access_token": "[REDACTED]",
1393 "token_type": token_type,
1394 "expires_in": expires_in,
1395 "refresh_token": if refresh_token.is_some() { "[REDACTED]" } else { "" },
1396 "scope": scopes,
1397 }),
1398 );
1399 Self::notify_event(client_id, event, app_state, llm_client, status_tx, dispatch).await;
1400
1401 Ok(())
1402 }
1403
1404 async fn handle_oauth_error(
1406 client_id: NetGetClientId,
1407 error: String,
1408 app_state: Arc<AppState>,
1409 llm_client: OllamaClient,
1410 status_tx: mpsc::UnboundedSender<String>,
1411 dispatch: Dispatch,
1412 ) -> Result<()> {
1413 Log::new(Some(&status_tx)).error(format!("OAuth2 client {} error: {}", client_id, error));
1414
1415 let event = Event::new(
1417 &OAUTH2_ERROR_EVENT,
1418 serde_json::json!({
1419 "error": error,
1420 "error_description": "",
1421 }),
1422 );
1423 Self::notify_event(client_id, event, app_state, llm_client, status_tx, dispatch).await;
1424
1425 Ok(())
1426 }
1427
1428 async fn notify_event(
1430 client_id: NetGetClientId,
1431 event: Event,
1432 app_state: Arc<AppState>,
1433 llm_client: OllamaClient,
1434 status_tx: mpsc::UnboundedSender<String>,
1435 dispatch: Dispatch,
1436 ) {
1437 match dispatch {
1438 Dispatch::Silent => {}
1440 Dispatch::Inline => {
1441 Self::raise_event(client_id, event, app_state, llm_client, status_tx).await
1442 }
1443 Dispatch::Deferred => {
1444 let registrar = app_state.clone();
1445 let handle = tokio::spawn(Self::raise_event(
1446 client_id, event, app_state, llm_client, status_tx,
1447 ));
1448 registrar.register_client_task(client_id, handle).await;
1450 }
1451 }
1452 }
1453
1454 fn raise_event(
1462 client_id: NetGetClientId,
1463 event: Event,
1464 app_state: Arc<AppState>,
1465 llm_client: OllamaClient,
1466 status_tx: mpsc::UnboundedSender<String>,
1467 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>> {
1468 Box::pin(async move {
1469 let protocol = Arc::new(OAuth2ClientProtocol::new());
1470 let instruction = app_state
1471 .get_instruction_for_client(client_id)
1472 .await
1473 .unwrap_or_default();
1474 let memory = app_state
1475 .get_memory_for_client(client_id)
1476 .await
1477 .unwrap_or_default();
1478
1479 match call_llm_for_client(
1480 &llm_client,
1481 &app_state,
1482 client_id.to_string(),
1483 &instruction,
1484 &memory,
1485 Some(&event),
1486 protocol.as_ref(),
1487 &status_tx,
1488 )
1489 .await
1490 {
1491 Ok(ClientLlmResult {
1492 actions,
1493 memory_updates,
1494 }) => {
1495 if let Some(mem) = memory_updates {
1496 app_state.set_memory_for_client(client_id, mem).await;
1497 }
1498
1499 use crate::llm::actions::client_trait::Client;
1509 for action in actions {
1510 match protocol.execute_action(action.clone()) {
1511 Ok(result) => {
1512 if let Err(e) = Self::apply_action(
1513 result,
1514 Dispatch::Silent,
1515 client_id,
1516 app_state.clone(),
1517 llm_client.clone(),
1518 status_tx.clone(),
1519 )
1520 .await
1521 {
1522 error!(
1523 "OAuth2 client {} follow-up action failed: {}",
1524 client_id, e
1525 );
1526 }
1527 }
1528 Err(e) => error!(
1529 "OAuth2 client {} rejected its own follow-up action: {}",
1530 client_id, e
1531 ),
1532 }
1533 }
1534 }
1535 Err(e) => {
1536 error!("LLM error for OAuth2 client {}: {}", client_id, e);
1537 }
1538 }
1539 })
1540 }
1541
1542 async fn command_loop(
1552 mut command_rx: mpsc::Receiver<ClientCommand>,
1553 client_id: NetGetClientId,
1554 app_state: Arc<AppState>,
1555 llm_client: OllamaClient,
1556 status_tx: mpsc::UnboundedSender<String>,
1557 ) {
1558 use crate::llm::actions::client_trait::Client;
1559 use crate::llm::actions::protocol_trait::Protocol;
1560
1561 let protocol = OAuth2ClientProtocol::new();
1562
1563 while let Some(command) = command_rx.recv().await {
1564 let action = command.action.clone();
1565 let outcome = match protocol.execute_action(action.clone()) {
1566 Err(e) => Ok(ClientSendOutcome::Rejected {
1567 error: e.to_string(),
1568 }),
1569 Ok(result) => Self::apply_action(
1570 result,
1571 Dispatch::Deferred,
1572 client_id,
1573 app_state.clone(),
1574 llm_client.clone(),
1575 status_tx.clone(),
1576 )
1577 .await
1578 .map(|applied| match applied {
1579 Applied::Disconnect => ClientSendOutcome::Disconnected,
1580 Applied::Ran(detail) => ClientSendOutcome::Executed { detail },
1581 }),
1582 };
1583
1584 let outcome_json = match &outcome {
1585 Ok(outcome) => serde_json::to_value(outcome).unwrap_or(serde_json::Value::Null),
1586 Err(e) => serde_json::json!({"error": e.to_string()}),
1587 };
1588 app_state
1589 .record_access_log(
1590 AccessLogOwner::Client(client_id.as_u32()),
1591 protocol.protocol_name(),
1592 None,
1593 "injected_action",
1594 action,
1595 vec![outcome_json],
1596 )
1597 .await;
1598
1599 let disconnect = matches!(outcome, Ok(ClientSendOutcome::Disconnected));
1600 if let Err(e) = &outcome {
1601 error!("OAuth2 client {} injected action failed: {}", client_id, e);
1602 let _ = status_tx.send(format!(
1603 "[WARN] Client {} injected action failed: {}",
1604 client_id, e
1605 ));
1606 }
1607 let _ = status_tx.send("__UPDATE_UI__".to_string());
1608 crate::client::command_support::reply(command, outcome);
1609
1610 if disconnect {
1611 break;
1612 }
1613 }
1614
1615 app_state.remove_client_handle(client_id).await;
1616 let _ = status_tx.send("__UPDATE_UI__".to_string());
1617 info!("OAuth2 client {} command loop ended", client_id);
1618 }
1619}