1use async_trait::async_trait;
2use serde_json::{Map, Value, json};
3use soma_domain::actions::{ACTION_SPECS, ActionTransport, SomaAction};
4use soma_provider_core::{
5 CliOverlay, DocsOverlay, McpOverlay, PaletteOverlay, ProviderCatalog, ProviderIdentity,
6 ProviderKind, ProviderManifest, ProviderPrompt, ProviderTool, RestOverlay,
7};
8
9use crate::{
10 SomaService, dispatch_action,
11 provider_errors::ProviderError,
12 provider_registry::{Provider, ProviderCall, ProviderOutput},
13};
14
15#[derive(Clone)]
18pub struct StaticRustProvider {
19 service: SomaService,
20 catalog: ProviderCatalog,
21}
22
23impl StaticRustProvider {
24 pub fn new(service: SomaService) -> Self {
26 Self {
27 service,
28 catalog: static_catalog(),
29 }
30 }
31
32 pub fn catalog_static() -> ProviderCatalog {
34 static_catalog()
35 }
36}
37
38#[async_trait]
39impl Provider for StaticRustProvider {
40 fn catalog(&self) -> ProviderCatalog {
41 self.catalog.clone()
42 }
43
44 async fn call(&self, call: ProviderCall) -> Result<ProviderOutput, ProviderError> {
45 let action = SomaAction::from_rest(&call.action, &call.params)
46 .or_else(|_| SomaAction::from_mcp_args(&action_params(&call.action, &call.params)))
47 .map_err(|error| {
48 ProviderError::validation(
49 "static-rust",
50 call.action.clone(),
51 "invalid_static_action_input",
52 error.to_string(),
53 )
54 })?;
55 let value = match action {
56 SomaAction::Help => crate::execute_service_action(&self.service, &action)
57 .await
58 .map_err(|error| ProviderError::execution("static-rust", call.action, error))?,
59 SomaAction::ElicitName
60 | SomaAction::ScaffoldIntent
61 | SomaAction::PythonEnvironmentStatus
62 | SomaAction::PythonEnvironmentPrunePlan { .. }
63 | SomaAction::PythonEnvironmentPrune { .. }
64 | SomaAction::PythonEnvironmentRepair { .. }
65 | SomaAction::PythonEnvironmentUpdate { .. }
66 | SomaAction::PythonWorkerStatus
67 | SomaAction::PythonWorkerCancel { .. }
68 | SomaAction::PythonWorkerReset { .. }
69 | SomaAction::PythonGenerationStatus
70 | SomaAction::PythonGenerationRollback { .. }
71 | SomaAction::PythonGraduationStatus { .. }
72 | SomaAction::PythonGraduationApply { .. } => {
73 return Err(ProviderError::validation(
74 "static-rust",
75 call.action,
76 "application_action_required",
77 "action requires the shared application control plane",
78 ));
79 }
80 other => dispatch_action(&self.service, &other, surface_label(call.surface))
81 .await
82 .map_err(|error| ProviderError::execution("static-rust", call.action, error))?,
83 };
84 Ok(ProviderOutput::json(value))
85 }
86}
87
88fn static_catalog() -> ProviderCatalog {
89 ProviderManifest {
90 schema_version: 1,
91 provider: ProviderIdentity {
92 name: "static-rust".to_owned(),
93 kind: ProviderKind::StaticRust,
94 title: Some("Built-in Rust actions".to_owned()),
95 description: Some("Native service actions compiled into Soma.".to_owned()),
96 homepage: None,
97 source: None,
98 version: None,
99 enabled: Some(true),
100 },
101 tools: ACTION_SPECS.iter().map(static_tool).collect(),
102 prompts: vec![ProviderPrompt {
110 name: "quick_start".to_owned(),
111 description: "Check the server status and get a personalised greeting to verify \
112 the MCP connection is working end-to-end."
113 .to_owned(),
114 template: None,
115 arguments_schema: None,
116 scope: None,
117 mcp: None,
118 examples: Vec::new(),
119 }],
120 resources: Vec::new(),
121 tasks: Vec::new(),
122 elicitation: Vec::new(),
123 env: Vec::new(),
124 capabilities: Default::default(),
125 docs: Some(DocsOverlay {
126 when_to_use: Some(
127 "Use for Soma built-in Rust actions, scaffold intent collection, MCP elicitation flows, and CLI/REST action reference."
128 .to_owned(),
129 ),
130 examples: Vec::new(),
131 troubleshooting: Vec::new(),
132 }),
133 plugin: None,
134 ui: None,
135 meta: json!({}),
136 }
137}
138
139fn static_tool(spec: &soma_domain::actions::ActionSpec) -> ProviderTool {
140 ProviderTool {
141 name: spec.name.to_owned(),
142 description: spec.description.to_owned(),
143 title: None,
144 input_schema: action_input_schema(spec),
145 output_schema: Some(action_output_schema(spec)),
146 scope: spec.required_scope.map(ToOwned::to_owned),
147 destructive: spec.destructive,
148 requires_admin: spec.requires_admin,
149 cost: Some(spec.cost.as_str().to_owned()),
150 env: Vec::new(),
151 limits: None,
152 mcp: Some(McpOverlay {
153 enabled: spec.transport.mcp(),
154 title: None,
155 annotations: json!({}),
156 }),
157 rest: static_rest_overlay(spec),
158 cli: spec.cli.map(|cli| CliOverlay {
159 enabled: spec.transport.cli(),
160 command: Some(cli.command.to_owned()),
161 aliases: Vec::new(),
162 about: Some(cli.description.to_owned()),
163 long_about: Some(cli.usage.to_owned()),
164 hidden: false,
165 flags: cli
166 .flags
167 .iter()
168 .map(|flag| {
169 json!({
170 "name": flag.name,
171 "value_name": flag.value_name,
172 "required": flag.required,
173 "description": flag.description,
174 })
175 })
176 .collect(),
177 default_output: None,
178 interactive: false,
179 }),
180 palette: Some(PaletteOverlay {
181 enabled: spec.transport != ActionTransport::McpOnly,
182 category: Some("Example".to_owned()),
183 icon: None,
184 tone: Some("neutral".to_owned()),
185 arg_mode: Some("schema".to_owned()),
186 result_view: Some("json".to_owned()),
187 aurora_blocks: Vec::new(),
188 }),
189 ui: None,
190 examples: Vec::new(),
191 meta: json!({
192 "returns": spec.returns,
193 "cli_usage": spec.cli.map(|cli| cli.usage),
194 "scaffold_fallback": if spec.name == "scaffold_intent" {
195 json!({
196 "recommended_skill": "scaffold-project",
197 "instructions": "Ask the user for the scaffold fields manually, then create the same JSON shape documented by the scaffold-project skill. Do not mutate files until the user approves the plan."
198 })
199 } else {
200 Value::Null
201 },
202 }),
203 }
204}
205
206fn static_rest_overlay(spec: &soma_domain::actions::ActionSpec) -> Option<RestOverlay> {
207 match spec.rest_path {
208 Some(path) => Some(RestOverlay {
209 enabled: spec.transport.rest(),
210 method: spec.rest_method.map(ToOwned::to_owned),
211 path: Some(path.to_owned()),
212 tags: vec!["soma".to_owned()],
213 summary: Some(spec.description.to_owned()),
214 description: Some(spec.description.to_owned()),
215 deprecated: false,
216 path_params: json!({}),
217 query_params: json!({}),
218 request_body_schema: None,
219 }),
220 None if !spec.transport.rest() => Some(RestOverlay {
221 enabled: false,
222 method: None,
223 path: None,
224 tags: vec!["soma".to_owned()],
225 summary: Some(spec.description.to_owned()),
226 description: Some(spec.description.to_owned()),
227 deprecated: false,
228 path_params: json!({}),
229 query_params: json!({}),
230 request_body_schema: None,
231 }),
232 None => None,
233 }
234}
235
236fn action_output_schema(spec: &soma_domain::actions::ActionSpec) -> Value {
237 match spec.name {
238 "greet" => json!({
239 "type": "object",
240 "additionalProperties": false,
241 "required": ["greeting", "target", "server"],
242 "properties": {
243 "greeting": { "type": "string" },
244 "target": { "type": "string" },
245 "server": { "type": "string" }
246 }
247 }),
248 "echo" => json!({
249 "type": "object",
250 "additionalProperties": false,
251 "required": ["echo"],
252 "properties": {
253 "echo": { "type": "string" }
254 }
255 }),
256 "status" => json!({
257 "type": "object",
258 "additionalProperties": true,
259 "required": ["status"],
260 "properties": {
261 "status": { "type": "string" },
262 "note": { "type": "string" },
263 "warnings": {
264 "type": "array",
265 "items": { "type": "string" }
266 }
267 }
268 }),
269 "elicit_name" => json!({
270 "type": "object",
271 "additionalProperties": true,
272 "properties": {
273 "greeting": { "type": "string" },
274 "name": { "type": "string" },
275 "message": { "type": "string" },
276 "note": { "type": "string" },
277 "hint": { "type": "string" },
278 "fallback_greeting": { "type": "string" }
279 }
280 }),
281 "scaffold_intent" => json!({
282 "type": "object",
283 "additionalProperties": true,
284 "properties": {
285 "kind": { "type": "string" },
286 "schema_version": { "type": "integer" },
287 "status": { "type": "string" },
288 "server_category": { "type": "string" },
289 "required_surfaces": {
290 "type": "array",
291 "items": { "type": "string" }
292 },
293 "project": { "type": "object" },
294 "upstream": { "type": "object" },
295 "runtime": { "type": "object" },
296 "mcp_primitives": {
297 "type": "array",
298 "items": { "type": "string" }
299 },
300 "handoff": { "type": "object" },
301 "policy": { "type": "object" }
302 }
303 }),
304 "help" => json!({
305 "type": "object",
306 "additionalProperties": false,
307 "required": ["actions", "mcp_only_actions", "catalog", "preferred_rest_style", "usage", "examples"],
308 "properties": {
309 "actions": {
310 "type": "array",
311 "items": { "type": "string" }
312 },
313 "mcp_only_actions": {
314 "type": "array",
315 "items": { "type": "string" }
316 },
317 "catalog": {
318 "type": "array",
319 "items": { "type": "object" }
320 },
321 "preferred_rest_style": { "type": "string" },
322 "usage": { "type": "string" },
323 "examples": { "type": "object" }
324 }
325 }),
326 _ => json!({
327 "type": "object",
328 "description": format!("Structured result for {}.", spec.returns),
329 "additionalProperties": true
330 }),
331 }
332}
333
334fn action_input_schema(spec: &soma_domain::actions::ActionSpec) -> Value {
335 let mut properties = Map::new();
336 let mut required = Vec::new();
337 for param in spec.params {
338 let json_type = match param.ty {
339 "integer" => "integer",
340 "number" => "number",
341 "boolean" => "boolean",
342 "object" => "object",
343 "array" => "array",
344 _ => "string",
345 };
346 let mut schema = json!({
347 "type": json_type,
348 "description": param.description,
349 });
350 if param.required && json_type == "string" {
351 schema["minLength"] = json!(1);
352 }
353 properties.insert(param.name.to_owned(), schema);
354 if param.required {
355 required.push(Value::String(param.name.to_owned()));
356 }
357 }
358 let mut schema = json!({
359 "type": "object",
360 "additionalProperties": false,
361 "properties": properties,
362 });
363 if !required.is_empty() {
364 schema["required"] = Value::Array(required);
365 }
366 schema
367}
368
369fn action_params(action: &str, params: &Value) -> Value {
370 let mut params = params.clone();
371 if let Value::Object(map) = &mut params {
372 map.insert("action".to_owned(), Value::String(action.to_owned()));
373 }
374 params
375}
376
377fn surface_label(surface: crate::provider_registry::ProviderSurface) -> &'static str {
378 match surface {
379 crate::provider_registry::ProviderSurface::Mcp => "mcp",
380 crate::provider_registry::ProviderSurface::Rest => "rest",
381 crate::provider_registry::ProviderSurface::Cli => "cli",
382 crate::provider_registry::ProviderSurface::Palette => "palette",
383 }
384}