1use serde_json::{json, Value};
2use thiserror::Error;
3
4use crate::dispatch_helpers::{structured_error, GatewayStructuredError};
5use crate::gateway::catalog::{GatewayAction, GatewayActionCatalog};
6use crate::gateway::manager::GatewayManager;
7#[cfg(feature = "oauth")]
8use crate::gateway::params::string_param;
9use crate::gateway::params::{
10 object_params, required_string_param, test_upstream_config_from_params,
11 upstream_config_from_params, ParamsError,
12};
13use crate::process::guard::SpawnGuard;
14use crate::process::stdio::StdioProcessSpec;
15use crate::upstream::pool::UpstreamPool;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct GatewayAccess {
19 pub read: bool,
20 pub admin: bool,
21}
22
23#[derive(Debug, Error)]
24pub enum GatewayDispatchError {
25 #[error("gateway admin access required")]
26 AdminRequired,
27 #[error(transparent)]
28 Params(#[from] ParamsError),
29 #[error("unknown gateway action")]
30 UnknownAction,
31 #[error("spawn validation failed")]
32 SpawnValidation,
33 #[error(transparent)]
34 Manager(#[from] crate::gateway::manager::GatewayManagerError),
35}
36
37impl GatewayDispatchError {
38 #[must_use]
39 pub fn structured(&self, action: &str) -> GatewayStructuredError {
40 match self {
41 Self::AdminRequired => structured_error(
42 action,
43 "admin_required",
44 "authorization",
45 "use a principal with gateway admin access",
46 ),
47 Self::Params(_) => structured_error(
48 action,
49 "invalid_param",
50 "validation",
51 "pass an object with valid gateway action parameters",
52 ),
53 Self::UnknownAction => structured_error(
54 action,
55 "unknown_action",
56 "validation",
57 "use one of the advertised gateway actions",
58 ),
59 Self::SpawnValidation => structured_error(
60 action,
61 "spawn_validation_failed",
62 "validation",
63 "use an allowed command and safe environment",
64 ),
65 Self::Manager(error) => manager_error_shape(action, error),
66 }
67 }
68}
69
70pub async fn dispatch_gateway_action(
71 manager: &GatewayManager,
72 access: GatewayAccess,
73 action_name: &str,
74 params: Value,
75) -> Result<Value, GatewayDispatchError> {
76 let catalog = GatewayActionCatalog::standard();
77 let action = catalog
78 .get(action_name)
79 .ok_or(GatewayDispatchError::UnknownAction)?;
80 enforce_access(action, access)?;
81 if action.spawn_validation_required {
82 validate_spawn_params(¶ms)?;
83 }
84 match action_name {
85 "gateway.list" => Ok(crate::gateway::view_models::gateway_list_view(manager).await?),
86 "gateway.config.view" => Ok(crate::gateway::view_models::gateway_config_view(manager)),
87 "gateway.add" => {
88 let view = manager.add_upstream(upstream_config_from_params(¶ms)?)?;
89 Ok(json!({"added": true, "config": view}))
90 }
91 "gateway.update" => {
92 let view = manager.update_upstream(upstream_config_from_params(¶ms)?)?;
93 Ok(json!({"updated": true, "config": view}))
94 }
95 "gateway.remove" => {
96 let params = object_params(¶ms)?;
97 let name = required_string_param(params, "name")?;
98 let view = manager.remove_upstream(&name)?;
99 Ok(json!({"removed": name, "config": view}))
100 }
101 "gateway.reload" => {
102 let view = manager.reload_from_store()?;
103 Ok(json!({"reloaded": true, "config": view}))
104 }
105 "gateway.test" => test_upstream_connectivity(¶ms).await,
106 "gateway.import.approve" => {
107 let view = manager.add_upstream(upstream_config_from_params(¶ms)?)?;
108 Ok(json!({"approved": true, "config": view}))
109 }
110 "gateway.oauth.start" => oauth_start(manager, ¶ms).await,
111 "gateway.oauth.status" => oauth_status(manager, ¶ms).await,
112 "gateway.oauth.clear" => oauth_clear(manager, ¶ms).await,
113 _ => Err(GatewayDispatchError::UnknownAction),
114 }
115}
116
117async fn oauth_start(
118 manager: &GatewayManager,
119 params: &Value,
120) -> Result<Value, GatewayDispatchError> {
121 #[cfg(feature = "oauth")]
122 {
123 let (upstream, subject) = oauth_params(params)?;
124 return Ok(serde_json::to_value(
125 manager
126 .begin_upstream_authorization(&upstream, &subject)
127 .await?,
128 )
129 .expect("begin authorization serializes"));
130 }
131 #[cfg(not(feature = "oauth"))]
132 {
133 let _ = (manager, params);
134 Err(GatewayDispatchError::Manager(
135 crate::gateway::manager::GatewayManagerError::OAuth(
136 "gateway.oauth.start requires the oauth feature".to_owned(),
137 ),
138 ))
139 }
140}
141
142async fn oauth_status(
143 manager: &GatewayManager,
144 params: &Value,
145) -> Result<Value, GatewayDispatchError> {
146 #[cfg(feature = "oauth")]
147 {
148 let (upstream, subject) = oauth_params(params)?;
149 return Ok(
150 serde_json::to_value(manager.upstream_oauth_status(&upstream, &subject).await?)
151 .expect("oauth status serializes"),
152 );
153 }
154 #[cfg(not(feature = "oauth"))]
155 {
156 let _ = (manager, params);
157 Err(GatewayDispatchError::Manager(
158 crate::gateway::manager::GatewayManagerError::OAuth(
159 "gateway.oauth.status requires the oauth feature".to_owned(),
160 ),
161 ))
162 }
163}
164
165async fn oauth_clear(
166 manager: &GatewayManager,
167 params: &Value,
168) -> Result<Value, GatewayDispatchError> {
169 #[cfg(feature = "oauth")]
170 {
171 let (upstream, subject) = oauth_params(params)?;
172 manager
173 .clear_upstream_credentials(&upstream, &subject)
174 .await?;
175 Ok(json!({"ok": true}))
176 }
177 #[cfg(not(feature = "oauth"))]
178 {
179 let _ = (manager, params);
180 Err(GatewayDispatchError::Manager(
181 crate::gateway::manager::GatewayManagerError::OAuth(
182 "gateway.oauth.clear requires the oauth feature".to_owned(),
183 ),
184 ))
185 }
186}
187
188#[cfg(feature = "oauth")]
189fn oauth_params(params: &Value) -> Result<(String, String), GatewayDispatchError> {
190 let params = object_params(params)?;
191 let upstream = required_string_param(params, "upstream")?;
192 let subject = string_param(params, "subject")?.unwrap_or_else(|| "gateway".to_owned());
193 Ok((upstream, subject))
194}
195
196async fn test_upstream_connectivity(params: &Value) -> Result<Value, GatewayDispatchError> {
197 let config = test_upstream_config_from_params(params)?;
198 let pool = UpstreamPool::default();
199 pool.register_config(config.clone()).map_err(|error| {
200 GatewayDispatchError::Manager(crate::gateway::manager::GatewayManagerError::Upstream(
201 error,
202 ))
203 })?;
204 let snapshot = pool
205 .discover_upstream(&config.name)
206 .await
207 .map_err(|error| {
208 GatewayDispatchError::Manager(crate::gateway::manager::GatewayManagerError::Upstream(
209 error,
210 ))
211 })?;
212 if !snapshot.health.is_routable() {
213 return Err(GatewayDispatchError::Manager(
214 crate::gateway::manager::GatewayManagerError::Upstream(
215 crate::upstream::UpstreamError::NotRoutable {
216 upstream: snapshot.name,
217 reason: format!("{:?}", snapshot.health),
218 },
219 ),
220 ));
221 }
222 Ok(json!({
223 "ok": true,
224 "upstream": snapshot.name,
225 "transport": snapshot.transport,
226 "tool_count": snapshot.tools.len(),
227 "resource_count": snapshot.resources.len(),
228 "prompt_count": snapshot.prompts.len(),
229 }))
230}
231
232fn enforce_access(
233 action: GatewayAction,
234 access: GatewayAccess,
235) -> Result<(), GatewayDispatchError> {
236 if action.admin_required && !access.admin {
237 return Err(GatewayDispatchError::AdminRequired);
238 }
239 if !(action.admin_required || access.read || access.admin) {
240 return Err(GatewayDispatchError::AdminRequired);
241 }
242 Ok(())
243}
244
245fn validate_spawn_params(params: &Value) -> Result<(), GatewayDispatchError> {
246 let config = test_upstream_config_from_params(params)?;
247 if let Some(command) = config.command {
248 let spec = StdioProcessSpec {
249 command,
250 args: config.args,
251 env: config.env,
252 };
253 spec.validate(&SpawnGuard::default())
254 .map_err(|_| GatewayDispatchError::SpawnValidation)?;
255 }
256 Ok(())
257}
258
259fn manager_error_shape(
260 action: &str,
261 error: &crate::gateway::manager::GatewayManagerError,
262) -> GatewayStructuredError {
263 use crate::gateway::manager::GatewayManagerError;
264 use crate::upstream::UpstreamError;
265
266 match error {
267 GatewayManagerError::GatewayReloading => structured_error(
268 action,
269 "gateway_reloading",
270 "runtime",
271 "retry after the gateway reload completes",
272 ),
273 GatewayManagerError::StoreNotMounted => structured_error(
274 action,
275 "store_not_mounted",
276 "configuration",
277 "start the host application with a mounted gateway config store",
278 ),
279 GatewayManagerError::UpstreamExists(_) => structured_error(
280 action,
281 "upstream_exists",
282 "validation",
283 "use gateway.update or remove the existing upstream first",
284 ),
285 GatewayManagerError::UpstreamMissing(_) => structured_error(
286 action,
287 "upstream_missing",
288 "validation",
289 "configure the upstream before operating on it",
290 ),
291 GatewayManagerError::Config(_) => structured_error(
292 action,
293 "invalid_config",
294 "validation",
295 "fix the gateway configuration and retry",
296 ),
297 GatewayManagerError::OAuth(_) => structured_error(
298 action,
299 "oauth_runtime_error",
300 "runtime",
301 "configure upstream OAuth resources and retry",
302 ),
303 GatewayManagerError::Upstream(UpstreamError::UnknownUpstream { .. }) => structured_error(
304 action,
305 "unknown_upstream",
306 "validation",
307 "configure the upstream before calling it",
308 ),
309 GatewayManagerError::Upstream(UpstreamError::NotExposed { .. }) => structured_error(
310 action,
311 "not_exposed",
312 "authorization",
313 "choose a tool/resource/prompt exposed by the upstream policy",
314 ),
315 GatewayManagerError::Upstream(UpstreamError::NotRoutable { .. }) => structured_error(
316 action,
317 "not_routable",
318 "runtime",
319 "wait for the upstream to connect or fix its configuration",
320 ),
321 GatewayManagerError::Upstream(UpstreamError::Unsupported { .. }) => structured_error(
322 action,
323 "unsupported_transport",
324 "unsupported",
325 "this gateway build cannot route that upstream transport yet",
326 ),
327 GatewayManagerError::Upstream(UpstreamError::LiveConnect { .. }) => structured_error(
328 action,
329 "upstream_connect_failed",
330 "runtime",
331 "fix the upstream configuration or wait for the server to become reachable",
332 ),
333 GatewayManagerError::Upstream(UpstreamError::LiveCall { .. }) => structured_error(
334 action,
335 "upstream_call_failed",
336 "runtime",
337 "retry after verifying the upstream server and requested capability",
338 ),
339 GatewayManagerError::Upstream(UpstreamError::ResponseTooLarge { .. }) => structured_error(
340 action,
341 "response_too_large",
342 "limits",
343 "narrow the request or lower the upstream response size",
344 ),
345 GatewayManagerError::Upstream(UpstreamError::ParamsMustBeObject) => structured_error(
346 action,
347 "invalid_param",
348 "validation",
349 "pass tool params as a JSON object",
350 ),
351 }
352}
353
354#[cfg(test)]
355#[path = "dispatch_tests.rs"]
356mod tests;