1use super::*;
21
22pub fn register(engine: &Arc<WshRpcEngine>, state: &AppState) {
23 register_skill_list(engine, state);
24 register_skill_get(engine, state);
25 register_skill_upsert(engine, state);
26 register_skill_delete(engine, state);
27 register_skill_bind(engine, state);
28 register_skill_unbind(engine, state);
29 register_skill_catalog_list(engine, state);
30 register_skill_catalog_upsert(engine, state);
31 register_skill_catalog_delete(engine, state);
32 register_skill_catalog_bind(engine, state);
33 register_skill_catalog_list_for_agent(engine, state);
34 register_skill_catalog_unbind(engine, state);
35 register_skill_catalog_bind_to_bundle(engine, state);
36 register_skill_catalog_unbind_from_bundle(engine, state);
37 register_skill_catalog_list_for_bundle(engine, state);
38 register_skill_catalog_upsert_for_bundle(engine, state);
39}
40
41fn register_skill_list(engine: &Arc<WshRpcEngine>, state: &AppState) {
42 let mstore = state.mstore.clone();
43 let identity_store = state.identity_store.clone();
44 engine.register_typed(
45 COMMAND_SKILL_LIST,
46 move |req: SkillAgentScopeData, ctx| {
47 let mstore = mstore.clone();
48 let identity_store = identity_store.clone();
49 async move {
50 check_s1(&mstore, &ctx, &req.agent_id)?;
51 let skills = mstore.skill_list(&identity_store, &req.agent_id)
52 .map_err(|e| format!("skill.list: {e}"))?;
53 Ok(skills)
54 }
55 },
56 );
57}
58
59fn register_skill_get(engine: &Arc<WshRpcEngine>, state: &AppState) {
60 let mstore = state.mstore.clone();
61 let identity_store = state.identity_store.clone();
62 engine.register_typed(
63 COMMAND_SKILL_GET,
64 move |req: SkillAgentItemData, ctx| {
65 let mstore = mstore.clone();
66 let identity_store = identity_store.clone();
67 async move {
68 check_s1(&mstore, &ctx, &req.agent_id)?;
69 if !mstore.skill_is_accessible_to(&identity_store, &req.agent_id, &req.id)
70 .map_err(|e| format!("skill.get: {e}"))?
71 {
72 return Err("FORBIDDEN: skill not accessible to this agent".to_string());
73 }
74 let skill = identity_store.skill_get(&req.id)
75 .map_err(|e| format!("skill.get: {e}"))?;
76 Ok(skill)
77 }
78 },
79 );
80}
81
82fn register_skill_upsert(engine: &Arc<WshRpcEngine>, state: &AppState) {
83 let mstore = state.mstore.clone();
84 let identity_store = state.identity_store.clone();
85 let broker = state.broker.clone();
86 engine.register_typed(
87 COMMAND_SKILL_UPSERT,
88 move |req: CommandSkillUpsertData, ctx| {
89 let mstore = mstore.clone();
90 let identity_store = identity_store.clone();
91 let broker = broker.clone();
92 async move {
93 check_s1(&mstore, &ctx, &req.agent_id)?;
94
95 let now = now_ms();
96 let (id, created_at) = if req.id.is_empty() {
100 (uuid::Uuid::new_v4().to_string(), now)
101 } else {
102 if !mstore.skill_is_bound_to(&req.agent_id, &req.id)
103 .map_err(|e| format!("skill.upsert: {e}"))?
104 {
105 return Err("FORBIDDEN: skill not bound to this agent".to_string());
106 }
107 let existing = identity_store.skill_get(&req.id)
108 .map_err(|e| format!("skill.upsert: {e}"))?;
109 match existing {
110 Some(s) if s.is_global => {
111 return Err("FORBIDDEN: cannot mutate a global skill".to_string());
112 }
113 Some(s) => (req.id.clone(), s.created_at),
114 None => (req.id.clone(), now),
115 }
116 };
117
118 let skill = crate::backend::storage::Skill {
119 id: id.clone(),
120 name: req.name,
121 trigger: req.trigger,
122 skill_type: req.skill_type,
123 description: req.description,
124 content: req.content,
125 is_global: false,
126 created_at,
127 updated_at: now,
128 };
129 mstore.skill_upsert_unique(&identity_store, &req.agent_id, &skill, req.id.is_empty())
134 .map_err(|e| format!("skill.upsert: {e}"))?;
135 broker.publish_event::<crate::backend::mps_events::SkillsChanged>(&(), vec![], 0);
136 Ok(skill)
137 }
138 },
139 );
140}
141
142fn register_skill_delete(engine: &Arc<WshRpcEngine>, state: &AppState) {
143 let mstore = state.mstore.clone();
144 let identity_store = state.identity_store.clone();
145 let broker = state.broker.clone();
146 engine.register_typed(
147 COMMAND_SKILL_DELETE,
148 move |req: SkillAgentItemData, ctx| {
149 let mstore = mstore.clone();
150 let identity_store = identity_store.clone();
151 let broker = broker.clone();
152 async move {
153 check_s1(&mstore, &ctx, &req.agent_id)?;
154 if !mstore.skill_is_bound_to(&req.agent_id, &req.id)
155 .map_err(|e| format!("skill.delete: {e}"))?
156 {
157 return Err("FORBIDDEN: skill not bound to this agent".to_string());
158 }
159 if let Some(existing) = identity_store.skill_get(&req.id)
160 .map_err(|e| format!("skill.delete: {e}"))?
161 {
162 if existing.is_global {
163 return Err("FORBIDDEN: cannot delete a global skill".to_string());
164 }
165 }
166 let deleted = mstore.skill_delete(&identity_store, &req.id)
167 .map_err(|e| format!("skill.delete: {e}"))?;
168 if deleted {
169 broker.publish_event::<crate::backend::mps_events::SkillsChanged>(&(), vec![], 0);
170 }
171 Ok(SkillDeleteResult { deleted })
172 }
173 },
174 );
175}
176
177fn register_skill_bind(engine: &Arc<WshRpcEngine>, state: &AppState) {
178 let mstore = state.mstore.clone();
179 let identity_store = state.identity_store.clone();
180 let broker = state.broker.clone();
181 engine.register_typed(
182 COMMAND_SKILL_BIND,
183 move |req: SkillAgentBindingData, ctx| {
184 let mstore = mstore.clone();
185 let identity_store = identity_store.clone();
186 let broker = broker.clone();
187 async move {
188 check_s1(&mstore, &ctx, &req.agent_id)?;
189 match identity_store.skill_get(&req.skill_id)
192 .map_err(|e| format!("skill.bind: {e}"))?
193 {
194 None => return Err("skill.bind: skill not found".to_string()),
195 Some(s) if !s.is_global => {
196 if !mstore.skill_is_bound_to(&req.agent_id, &req.skill_id)
197 .map_err(|e| format!("skill.bind: {e}"))?
198 {
199 return Err("FORBIDDEN: can only bind global skills".to_string());
200 }
201 }
202 Some(_) => {}
203 }
204 mstore.skill_bind(&identity_store, &req.agent_id, &req.skill_id)
205 .map_err(|e| format!("skill.bind: {e}"))?;
206 broker.publish_event::<crate::backend::mps_events::SkillsChanged>(&(), vec![], 0);
211 Ok(SkillBindResult { bound: true })
212 }
213 },
214 );
215}
216
217fn register_skill_unbind(engine: &Arc<WshRpcEngine>, state: &AppState) {
218 let mstore = state.mstore.clone();
219 let broker = state.broker.clone();
220 engine.register_typed(
221 COMMAND_SKILL_UNBIND,
222 move |req: SkillAgentBindingData, ctx| {
223 let mstore = mstore.clone();
224 let broker = broker.clone();
225 async move {
226 check_s1(&mstore, &ctx, &req.agent_id)?;
227 let unbound = mstore.skill_unbind(&req.agent_id, &req.skill_id)
228 .map_err(|e| format!("skill.unbind: {e}"))?;
229 if unbound {
230 broker.publish_event::<crate::backend::mps_events::SkillsChanged>(&(), vec![], 0);
231 }
232 Ok(SkillUnbindResult { unbound })
233 }
234 },
235 );
236}
237
238fn register_skill_catalog_list(engine: &Arc<WshRpcEngine>, state: &AppState) {
239 let mstore = state.mstore.clone();
240 let identity_store = state.identity_store.clone();
241 engine.register_typed(
242 COMMAND_SKILL_CATALOG_LIST,
243 move |_req: SkillCatalogListData, _ctx| {
244 let mstore = mstore.clone();
245 let identity_store = identity_store.clone();
246 async move {
247 let skills = mstore.skill_list_global(&identity_store)
248 .map_err(|e| format!("skill.catalog.list: {e}"))?;
249 Ok(skills)
250 }
251 },
252 );
253}
254
255fn register_skill_catalog_upsert(engine: &Arc<WshRpcEngine>, state: &AppState) {
256 let identity_store = state.identity_store.clone();
257 let broker = state.broker.clone();
258 engine.register_typed(
259 COMMAND_SKILL_CATALOG_UPSERT,
260 move |req: CommandSkillCatalogUpsertData, _ctx| {
261 let identity_store = identity_store.clone();
262 let broker = broker.clone();
263 async move {
264
265 let now = now_ms();
266 let (id, created_at) = if req.id.is_empty() {
267 (uuid::Uuid::new_v4().to_string(), now)
268 } else {
269 let existing = identity_store.skill_get(&req.id)
270 .map_err(|e| format!("skill.catalog.upsert: {e}"))?;
271 match existing {
272 Some(s) if !s.is_global => {
277 return Err("FORBIDDEN: cannot promote a private skill via the catalog".to_string());
278 }
279 Some(s) => (req.id.clone(), s.created_at),
280 None => (req.id.clone(), now),
281 }
282 };
283
284 let skill = crate::backend::storage::Skill {
285 id: id.clone(),
286 name: req.name,
287 trigger: req.trigger,
288 skill_type: req.skill_type,
289 description: req.description,
290 content: req.content,
291 is_global: true,
292 created_at,
293 updated_at: now,
294 };
295 identity_store.skill_upsert_unique_global(&skill)
300 .map_err(|e| format!("skill.catalog.upsert: {e}"))?;
301 broker.publish_event::<crate::backend::mps_events::SkillsChanged>(&(), vec![], 0);
302 Ok(skill)
303 }
304 },
305 );
306}
307
308fn register_skill_catalog_bind(engine: &Arc<WshRpcEngine>, state: &AppState) {
319 let mstore = state.mstore.clone();
320 let identity_store = state.identity_store.clone();
321 let broker = state.broker.clone();
322 engine.register_typed(
323 COMMAND_SKILL_CATALOG_BIND,
324 move |req: SkillAgentBindingData, _ctx| {
325 let mstore = mstore.clone();
326 let identity_store = identity_store.clone();
327 let broker = broker.clone();
328 async move {
329 match identity_store.skill_get(&req.skill_id)
333 .map_err(|e| format!("skill.catalog.bind: {e}"))?
334 {
335 None => return Err("skill.catalog.bind: skill not found".to_string()),
336 Some(s) if !s.is_global => {
337 if !mstore.skill_is_bound_to(&req.agent_id, &req.skill_id)
338 .map_err(|e| format!("skill.catalog.bind: {e}"))?
339 {
340 return Err("FORBIDDEN: can only bind global skills".to_string());
341 }
342 }
343 Some(_) => {}
344 }
345 mstore.skill_bind(&identity_store, &req.agent_id, &req.skill_id)
346 .map_err(|e| format!("skill.catalog.bind: {e}"))?;
347 broker.publish_event::<crate::backend::mps_events::SkillsChanged>(&(), vec![], 0);
351 Ok(SkillBindResult { bound: true })
352 }
353 },
354 );
355}
356
357fn register_skill_catalog_list_for_agent(engine: &Arc<WshRpcEngine>, state: &AppState) {
371 let mstore = state.mstore.clone();
372 let identity_store = state.identity_store.clone();
373 engine.register_typed(
374 COMMAND_SKILL_CATALOG_LIST_FOR_AGENT,
375 move |req: SkillAgentScopeData, _ctx| {
376 let mstore = mstore.clone();
377 let identity_store = identity_store.clone();
378 async move {
379 let skills = mstore.skill_list_global_for_agent(&identity_store, &req.agent_id)
380 .map_err(|e| format!("skill.catalog.list_for_agent: {e}"))?;
381 Ok(skills)
382 }
383 },
384 );
385}
386
387fn register_skill_catalog_unbind(engine: &Arc<WshRpcEngine>, state: &AppState) {
398 let mstore = state.mstore.clone();
399 let identity_store = state.identity_store.clone();
400 let broker = state.broker.clone();
401 engine.register_typed(
402 COMMAND_SKILL_CATALOG_UNBIND,
403 move |req: SkillAgentBindingData, _ctx| {
404 let mstore = mstore.clone();
405 let identity_store = identity_store.clone();
406 let broker = broker.clone();
407 async move {
408 match identity_store.skill_get(&req.skill_id)
409 .map_err(|e| format!("skill.catalog.unbind: {e}"))?
410 {
411 None => return Err("skill.catalog.unbind: skill not found".to_string()),
412 Some(s) if !s.is_global => {
413 return Err("FORBIDDEN: can only unbind global skills".to_string());
414 }
415 Some(_) => {}
416 }
417 let unbound = mstore.skill_unbind(&req.agent_id, &req.skill_id)
418 .map_err(|e| format!("skill.catalog.unbind: {e}"))?;
419 if unbound {
420 broker.publish_event::<crate::backend::mps_events::SkillsChanged>(&(), vec![], 0);
421 }
422 Ok(SkillUnbindResult { unbound })
423 }
424 },
425 );
426}
427
428fn register_skill_catalog_bind_to_bundle(engine: &Arc<WshRpcEngine>, state: &AppState) {
434 let mstore = state.mstore.clone();
435 let id_store = state.id_store.clone();
436 let identity_store = state.identity_store.clone();
437 let broker = state.broker.clone();
438 engine.register_typed(
439 COMMAND_SKILL_CATALOG_BIND_TO_BUNDLE,
440 move |req: SkillBundleBindingData, _ctx| {
441 let mstore = mstore.clone();
442 let id_store = id_store.clone();
443 let identity_store = identity_store.clone();
444 let broker = broker.clone();
445 async move {
446 match identity_store.skill_get(&req.skill_id)
451 .map_err(|e| format!("skill.catalog.bind_to_bundle: {e}"))?
452 {
453 None => return Err("skill.catalog.bind_to_bundle: skill not found".to_string()),
454 Some(s) if !s.is_global => {
455 if !mstore.bundle_skill_is_accessible_to(&identity_store, &req.bundle_id, &req.skill_id)
456 .map_err(|e| format!("skill.catalog.bind_to_bundle: {e}"))?
457 {
458 return Err("FORBIDDEN: can only bind global skills to a bundle".to_string());
459 }
460 }
461 Some(_) => {}
462 }
463 mstore.bundle_skill_bind(&identity_store, &id_store, &req.bundle_id, &req.skill_id)
464 .map_err(|e| format!("skill.catalog.bind_to_bundle: {e}"))?;
465 broker.publish_event::<crate::backend::mps_events::SkillsChanged>(&(), vec![], 0);
466 Ok(SkillBindResult { bound: true })
467 }
468 },
469 );
470}
471
472fn register_skill_catalog_list_for_bundle(engine: &Arc<WshRpcEngine>, state: &AppState) {
479 let mstore = state.mstore.clone();
480 let identity_store = state.identity_store.clone();
481 engine.register_typed(
482 COMMAND_SKILL_CATALOG_LIST_FOR_BUNDLE,
483 move |req: SkillBundleScopeData, _ctx| {
484 let mstore = mstore.clone();
485 let identity_store = identity_store.clone();
486 async move {
487 let skills = mstore.bundle_skill_list(&identity_store, &req.bundle_id)
488 .map_err(|e| format!("skill.catalog.list_for_bundle: {e}"))?;
489 Ok(skills)
490 }
491 },
492 );
493}
494
495fn register_skill_catalog_upsert_for_bundle(engine: &Arc<WshRpcEngine>, state: &AppState) {
500 let mstore = state.mstore.clone();
501 let id_store = state.id_store.clone();
502 let identity_store = state.identity_store.clone();
503 let broker = state.broker.clone();
504 engine.register_typed(
505 COMMAND_SKILL_CATALOG_UPSERT_FOR_BUNDLE,
506 move |req: CommandSkillCatalogUpsertForBundleData, _ctx| {
507 let mstore = mstore.clone();
508 let id_store = id_store.clone();
509 let identity_store = identity_store.clone();
510 let broker = broker.clone();
511 async move {
512
513 let now = now_ms();
514 let (id, created_at) = if req.id.is_empty() {
515 (uuid::Uuid::new_v4().to_string(), now)
516 } else {
517 if !mstore.bundle_skill_is_bound_to(&req.bundle_id, &req.id)
518 .map_err(|e| format!("skill.catalog.upsert_for_bundle: {e}"))?
519 {
520 return Err("FORBIDDEN: skill not bound to this bundle".to_string());
521 }
522 let existing = identity_store.skill_get(&req.id)
523 .map_err(|e| format!("skill.catalog.upsert_for_bundle: {e}"))?;
524 match existing {
525 Some(s) if s.is_global => {
526 return Err("FORBIDDEN: cannot mutate a global skill".to_string());
527 }
528 Some(s) => (req.id.clone(), s.created_at),
529 None => (req.id.clone(), now),
530 }
531 };
532
533 let skill = crate::backend::storage::Skill {
534 id: id.clone(),
535 name: req.name,
536 trigger: req.trigger,
537 skill_type: req.skill_type,
538 description: req.description,
539 content: req.content,
540 is_global: false,
541 created_at,
542 updated_at: now,
543 };
544 mstore.bundle_skill_upsert_unique(&identity_store, &id_store, &req.bundle_id, &skill, req.id.is_empty())
545 .map_err(|e| format!("skill.catalog.upsert_for_bundle: {e}"))?;
546 broker.publish_event::<crate::backend::mps_events::SkillsChanged>(&(), vec![], 0);
547 Ok(skill)
548 }
549 },
550 );
551}
552
553fn register_skill_catalog_unbind_from_bundle(engine: &Arc<WshRpcEngine>, state: &AppState) {
557 let mstore = state.mstore.clone();
558 let broker = state.broker.clone();
559 engine.register_typed(
560 COMMAND_SKILL_CATALOG_UNBIND_FROM_BUNDLE,
561 move |req: SkillBundleBindingData, _ctx| {
562 let mstore = mstore.clone();
563 let broker = broker.clone();
564 async move {
565 let unbound = mstore.bundle_skill_unbind(&req.bundle_id, &req.skill_id)
566 .map_err(|e| format!("skill.catalog.unbind_from_bundle: {e}"))?;
567 if unbound {
568 broker.publish_event::<crate::backend::mps_events::SkillsChanged>(&(), vec![], 0);
569 }
570 Ok(SkillUnbindResult { unbound })
571 }
572 },
573 );
574}
575
576fn register_skill_catalog_delete(engine: &Arc<WshRpcEngine>, state: &AppState) {
577 let mstore = state.mstore.clone();
578 let identity_store = state.identity_store.clone();
579 let broker = state.broker.clone();
580 engine.register_typed(
581 COMMAND_SKILL_CATALOG_DELETE,
582 move |req: SkillCatalogItemData, _ctx| {
583 let mstore = mstore.clone();
584 let identity_store = identity_store.clone();
585 let broker = broker.clone();
586 async move {
587 if let Some(existing) = identity_store.skill_get(&req.id)
588 .map_err(|e| format!("skill.catalog.delete: {e}"))?
589 {
590 if !existing.is_global {
591 return Err("FORBIDDEN: cannot delete a private skill via the catalog".to_string());
592 }
593 }
594 let deleted = mstore.skill_delete(&identity_store, &req.id)
595 .map_err(|e| format!("skill.catalog.delete: {e}"))?;
596 if deleted {
597 broker.publish_event::<crate::backend::mps_events::SkillsChanged>(&(), vec![], 0);
598 }
599 Ok(SkillDeleteResult { deleted })
600 }
601 },
602 );
603}