1use crate::credentials;
2use anyhow::{Result, anyhow};
3use serde::{Deserialize, Serialize};
4
5pub struct AdminRunOutput {
6 pub status: u16,
7 pub content_type: Option<String>,
8 pub body: Vec<u8>,
9}
10
11#[derive(Serialize)]
12struct AdminRunInput<'a> {
13 project_id: &'a str,
14 task: &'a str,
15 input: serde_json::Value,
16}
17
18#[derive(Deserialize)]
19#[serde(tag = "t", rename_all_fields = "camelCase")]
20enum AdminRunResponse {
21 Ok {
22 status: u16,
23 #[serde(default)]
24 content_type: Option<String>,
25 #[serde(default)]
26 body: String,
27 },
28 NotLoggedIn,
29 NotFound,
30 Forbidden,
31 NotDeployed,
32 UpstreamError {
33 status: u16,
34 #[serde(default)]
35 content_type: Option<String>,
36 #[serde(default)]
37 body: String,
38 },
39 InternalError {
40 reason: String,
41 },
42}
43
44pub async fn admin_run(
45 project_id: &str,
46 task: &str,
47 input_body: Vec<u8>,
48 timeout_secs: u64,
49) -> Result<AdminRunOutput> {
50 let creds = credentials::require()?;
51
52 let input: serde_json::Value = if input_body.is_empty() {
53 serde_json::Value::Null
54 } else {
55 serde_json::from_slice(&input_body)
56 .map_err(|e| anyhow!("admin run input is not valid JSON: {e}"))?
57 };
58
59 let url = format!(
60 "{}/__forte_action/admin_run",
61 creds.control_url.trim_end_matches('/')
62 );
63
64 let client = reqwest::Client::builder()
65 .timeout(std::time::Duration::from_secs(timeout_secs))
66 .build()?;
67
68 let raw: AdminRunResponse = client
69 .post(&url)
70 .bearer_auth(&creds.token)
71 .json(&AdminRunInput {
72 project_id,
73 task,
74 input,
75 })
76 .send()
77 .await?
78 .error_for_status()
79 .map_err(|e| anyhow!("admin run control call failed: {e}"))?
80 .json()
81 .await?;
82
83 match raw {
84 AdminRunResponse::Ok {
85 status,
86 content_type,
87 body,
88 } => Ok(AdminRunOutput {
89 status,
90 content_type,
91 body: body.into_bytes(),
92 }),
93 AdminRunResponse::UpstreamError {
94 status,
95 content_type,
96 body,
97 } => Ok(AdminRunOutput {
98 status,
99 content_type,
100 body: body.into_bytes(),
101 }),
102 AdminRunResponse::NotLoggedIn => {
103 Err(anyhow!("control rejected token; run `fn0 login` again."))
104 }
105 AdminRunResponse::NotFound => Err(anyhow!("project '{project_id}' not found.")),
106 AdminRunResponse::Forbidden => Err(anyhow!(
107 "project '{project_id}' is not owned by the signed-in user."
108 )),
109 AdminRunResponse::NotDeployed => Err(anyhow!(
110 "project '{project_id}' has no deployed version yet."
111 )),
112 AdminRunResponse::InternalError { reason } => {
113 Err(anyhow!("control admin_run internal error: {reason}"))
114 }
115 }
116}