Skip to main content

soma_gateway/gateway/
dispatch.rs

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(&params)?;
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(&params)?)?;
89            Ok(json!({"added": true, "config": view}))
90        }
91        "gateway.update" => {
92            let view = manager.update_upstream(upstream_config_from_params(&params)?)?;
93            Ok(json!({"updated": true, "config": view}))
94        }
95        "gateway.remove" => {
96            let params = object_params(&params)?;
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(&params).await,
106        "gateway.import.approve" => {
107            let view = manager.add_upstream(upstream_config_from_params(&params)?)?;
108            Ok(json!({"approved": true, "config": view}))
109        }
110        "gateway.oauth.start" => oauth_start(manager, &params).await,
111        "gateway.oauth.status" => oauth_status(manager, &params).await,
112        "gateway.oauth.clear" => oauth_clear(manager, &params).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;