zeroclaw_gateway/
api_quickstart.rs1use axum::{
14 Json,
15 extract::State,
16 http::{HeaderMap, StatusCode},
17 response::IntoResponse,
18};
19use serde::{Deserialize, Serialize};
20use zeroclaw_config::presets::BuilderSubmission;
21use zeroclaw_runtime::quickstart::{
22 AppliedAgent, QuickstartError, QuickstartStep, Surface, apply_with_surface, record_dismissed,
23 validate_only_with_surface,
24};
25
26use super::AppState;
27use super::api::require_auth;
28
29#[derive(Debug, Serialize)]
30#[serde(tag = "kind", rename_all = "snake_case")]
31pub enum ValidateResult {
32 Ok,
33 Errors { errors: Vec<QuickstartError> },
34}
35
36#[derive(Debug, Serialize)]
37#[serde(tag = "kind", rename_all = "snake_case")]
38pub enum ApplyResult {
39 Applied {
40 agent: AppliedAgent,
41 daemon_restarted: bool,
47 },
48 Errors {
49 errors: Vec<QuickstartError>,
50 },
51}
52
53pub async fn handle_state(State(state): State<AppState>, headers: HeaderMap) -> impl IntoResponse {
59 if let Err(e) = require_auth(&state, &headers) {
60 return e.into_response();
61 }
62 let cfg = state.config.read().clone();
63 let body = zeroclaw_runtime::quickstart::snapshot_state(&cfg);
64 (StatusCode::OK, Json(body)).into_response()
65}
66
67#[derive(Debug, Deserialize)]
68pub struct FieldsRequest {
69 pub section: zeroclaw_runtime::quickstart::FieldSection,
70 pub type_key: String,
71}
72
73#[derive(Debug, Serialize)]
74pub struct FieldsResult {
75 pub fields: Vec<zeroclaw_runtime::quickstart::FieldDescriptor>,
76}
77
78pub async fn handle_fields(
79 State(state): State<AppState>,
80 headers: HeaderMap,
81 Json(req): Json<FieldsRequest>,
82) -> impl IntoResponse {
83 if let Err(e) = require_auth(&state, &headers) {
84 return e.into_response();
85 }
86 let body = FieldsResult {
87 fields: zeroclaw_runtime::quickstart::field_shape(req.section, &req.type_key),
88 };
89 (StatusCode::OK, Json(body)).into_response()
90}
91
92pub async fn handle_validate(
93 State(state): State<AppState>,
94 headers: HeaderMap,
95 Json(submission): Json<BuilderSubmission>,
96) -> impl IntoResponse {
97 if let Err(e) = require_auth(&state, &headers) {
98 return e.into_response();
99 }
100 let cfg = state.config.read().clone();
101 let body = match validate_only_with_surface(&submission, &cfg, Surface::Web) {
102 Ok(()) => ValidateResult::Ok,
103 Err(errors) => ValidateResult::Errors { errors },
104 };
105 (StatusCode::OK, Json(body)).into_response()
106}
107
108#[derive(Debug, Deserialize)]
109pub struct DismissRequest {
110 pub run_id: String,
111 pub surface: Surface,
117 #[serde(default)]
120 pub last_step: Option<QuickstartStep>,
121}
122
123pub async fn handle_dismiss(
124 State(state): State<AppState>,
125 headers: HeaderMap,
126 Json(req): Json<DismissRequest>,
127) -> impl IntoResponse {
128 if let Err(e) = require_auth(&state, &headers) {
129 return e.into_response();
130 }
131 record_dismissed(&req.run_id, req.surface, req.last_step);
132 (StatusCode::NO_CONTENT, ()).into_response()
133}
134
135pub async fn handle_apply(
136 State(state): State<AppState>,
137 headers: HeaderMap,
138 Json(submission): Json<BuilderSubmission>,
139) -> impl IntoResponse {
140 if let Err(e) = require_auth(&state, &headers) {
141 return e.into_response();
142 }
143 let mut working = state.config.read().clone();
144 let result = apply_with_surface(submission, &mut working, Surface::Web).await;
145 let body = match result {
146 Ok(agent) => {
147 *state.config.write() = working;
148 state
149 .pending_reload
150 .store(true, std::sync::atomic::Ordering::Relaxed);
151 let reload_signalled = signal_daemon_reload(&state);
152 ApplyResult::Applied {
153 agent,
154 daemon_restarted: reload_signalled,
155 }
156 }
157 Err(errors) => ApplyResult::Errors { errors },
158 };
159 (StatusCode::OK, Json(body)).into_response()
160}
161
162fn signal_daemon_reload(state: &AppState) -> bool {
168 let Some(reload_tx) = state.reload_tx.clone() else {
169 ::zeroclaw_log::record!(
170 WARN,
171 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
172 .with_outcome(::zeroclaw_log::EventOutcome::Unknown)
173 .with_attrs(::serde_json::json!({
174 "reason": "no_supervisor",
175 })),
176 "quickstart: daemon reload not available (standalone gateway)"
177 );
178 return false;
179 };
180 ::zeroclaw_log::record!(
181 INFO,
182 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Start),
183 "quickstart: daemon reload signalled"
184 );
185 let shutdown_tx = state.shutdown_tx.clone();
186 state
187 .pending_reload
188 .store(false, std::sync::atomic::Ordering::Relaxed);
189 let started = std::time::Instant::now();
190 zeroclaw_spawn::spawn!(async move {
191 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
192 let _ = shutdown_tx.send(true);
193 let _ = reload_tx.send(true);
194 ::zeroclaw_log::record!(
195 INFO,
196 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Complete)
197 .with_outcome(::zeroclaw_log::EventOutcome::Success)
198 .with_attrs(::serde_json::json!({
199 "elapsed_ms": started.elapsed().as_millis() as u64,
200 })),
201 "quickstart: daemon reload dispatched"
202 );
203 });
204 true
205}
206
207