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 body: serde_json::Value,
24 },
25 NotLoggedIn,
26 NotFound,
27 Forbidden,
28 NotDeployed,
29 UpstreamError {
30 status: u16,
31 body: serde_json::Value,
32 },
33 InternalError {
34 reason: String,
35 },
36}
37
38pub async fn admin_run(
39 project_id: &str,
40 task: &str,
41 input_body: Vec<u8>,
42 timeout_secs: u64,
43) -> Result<AdminRunOutput> {
44 let creds = credentials::require()?;
45
46 let input: serde_json::Value = if input_body.is_empty() {
47 serde_json::Value::Null
48 } else {
49 serde_json::from_slice(&input_body)
50 .map_err(|e| anyhow!("admin run input is not valid JSON: {e}"))?
51 };
52
53 let url = format!(
54 "{}/__forte_action/admin_run",
55 creds.control_url.trim_end_matches('/')
56 );
57
58 let client = reqwest::Client::builder()
59 .timeout(std::time::Duration::from_secs(timeout_secs))
60 .build()?;
61
62 let raw: AdminRunResponse = client
63 .post(&url)
64 .bearer_auth(&creds.token)
65 .json(&AdminRunInput {
66 project_id,
67 task,
68 input,
69 })
70 .send()
71 .await?
72 .error_for_status()
73 .map_err(|e| anyhow!("admin run control call failed: {e}"))?
74 .json()
75 .await?;
76
77 match raw {
78 AdminRunResponse::Ok { status, body } => Ok(AdminRunOutput {
79 status,
80 content_type: Some("application/json".to_string()),
81 body: serde_json::to_vec(&body)?,
82 }),
83 AdminRunResponse::UpstreamError { status, body } => Ok(AdminRunOutput {
84 status,
85 content_type: Some("application/json".to_string()),
86 body: serde_json::to_vec(&body)?,
87 }),
88 AdminRunResponse::NotLoggedIn => {
89 Err(anyhow!("control rejected token; run `fn0 login` again."))
90 }
91 AdminRunResponse::NotFound => Err(anyhow!("project '{project_id}' not found.")),
92 AdminRunResponse::Forbidden => Err(anyhow!(
93 "project '{project_id}' is not owned by the signed-in user."
94 )),
95 AdminRunResponse::NotDeployed => Err(anyhow!(
96 "project '{project_id}' has no deployed version yet."
97 )),
98 AdminRunResponse::InternalError { reason } => {
99 Err(anyhow!("control admin_run internal error: {reason}"))
100 }
101 }
102}