Skip to main content

cookie_agent_engine/runtime/
skills.rs

1use std::{
2    collections::{BTreeMap, HashMap, HashSet},
3    path::Path,
4    sync::{Arc, Mutex},
5};
6
7use cookie_agent_config::{SkillContext, SkillDocument};
8use cookie_agent_protocol::{
9    EventPayload, PermissionAction, PermissionEffect, PermissionRule, RunId, SessionId,
10    SessionPermissionOverlay, Sha256Digest, SkillDescriptor, SkillsGetResult, SkillsListResult,
11    ToolCallId,
12};
13
14use super::{ActiveRun, Engine, EngineError, prompt_blocks::push_prompt_block};
15use crate::{ToolCall, TurnAgentContext};
16use crate::{permissions, policy::FrozenRunPolicy};
17pub const RESERVED_STAGED_SKILL_PREFIX: &str = "\0cookie-staged-skill:";
18
19/// Skill runtime state owned by [`super::Inner`].
20#[derive(Default)]
21pub(crate) struct SkillRuntimeState {
22    pub(crate) grants: Mutex<HashMap<SessionId, BTreeMap<String, SkillGrantOverlay>>>,
23    pub(crate) models: Mutex<HashMap<SessionId, cookie_agent_protocol::ModelKey>>,
24    pub(crate) pending_forks: Mutex<HashMap<ToolCallId, PreparedSkillInvocation>>,
25    pub(crate) pending_child: Mutex<HashMap<SessionId, PreparedSkillInvocation>>,
26    pub(crate) direct_calls: Mutex<HashSet<ToolCallId>>,
27}
28
29#[derive(Clone, Debug)]
30pub struct SkillInvocation {
31    pub name: String,
32    pub rendered: String,
33    pub context: Option<SkillContext>,
34}
35
36#[derive(Clone, Debug)]
37pub(crate) struct SkillGrantOverlay {
38    source: String,
39    overlay: SessionPermissionOverlay,
40}
41
42#[derive(Clone, Debug)]
43pub(crate) struct PreparedSkillInvocation {
44    name: String,
45    args: String,
46    rendered: String,
47    source_path: String,
48    base_dir: String,
49    supporting_files: Vec<String>,
50    grants: Vec<PermissionRule>,
51    model: Option<cookie_agent_protocol::ModelKey>,
52    context: Option<SkillContext>,
53}
54
55impl PreparedSkillInvocation {
56    pub(super) fn grants(&self) -> Option<SessionPermissionOverlay> {
57        (!self.grants.is_empty()).then(|| SessionPermissionOverlay {
58            rules: self.grants.clone(),
59        })
60    }
61
62    pub(crate) fn staged_payload(&self) -> cookie_agent_protocol::StagedSkillPayload {
63        cookie_agent_protocol::StagedSkillPayload {
64            provenance: cookie_agent_protocol::StagedSkillProvenance::SkillFork,
65            name: self.name.clone(),
66            args: self.args.clone(),
67            rendered_body: self.rendered.clone(),
68            source_path: self.source_path.clone(),
69            base_dir: self.base_dir.clone(),
70            supporting_files: self.supporting_files.clone(),
71            grants: self.grants.clone(),
72            model: self.model.clone(),
73        }
74    }
75}
76
77impl Engine {
78    #[must_use]
79    pub fn is_reserved_staged_skill_prompt(prompt: &str) -> bool {
80        prompt.starts_with(RESERVED_STAGED_SKILL_PREFIX)
81    }
82
83    pub fn skill_tool_available(&self, session: SessionId) -> Result<bool, EngineError> {
84        let projection = self.inner.store.get(session)?;
85        let policy = permissions::governing_agent_for_skills(&projection);
86        let grants = self.skill_grants_for_session(session);
87        Ok(self.inner.skills.skills().any(|(name, skill)| {
88            model_skill_visibility(
89                &policy,
90                &projection.permission_overlay,
91                grants.as_ref(),
92                self.inner.store.cwd(),
93                name,
94                skill,
95            )
96            .1
97        }))
98    }
99
100    pub(crate) fn skill_grants_for_session(
101        &self,
102        session: SessionId,
103    ) -> Option<SessionPermissionOverlay> {
104        self.inner
105            .skills_runtime
106            .grants
107            .lock()
108            .unwrap_or_else(|poisoned| poisoned.into_inner())
109            .get(&session)
110            .map(|grants| {
111                let rules = grants
112                    .values()
113                    .flat_map(|grant| {
114                        debug_assert!(grant.source.starts_with("skill:"));
115                        grant.overlay.rules.iter().cloned()
116                    })
117                    .collect();
118                SessionPermissionOverlay { rules }
119            })
120    }
121
122    pub fn list_skills(&self, session: SessionId) -> Result<SkillsListResult, EngineError> {
123        let projection = self.inner.store.get(session)?;
124        let policy = permissions::governing_agent_for_skills(&projection);
125        let grants = self.skill_grants_for_session(session);
126        let mut skills = self
127            .inner
128            .skills
129            .discoveries()
130            .iter()
131            .filter_map(|discovery| {
132                let skill = self.inner.skills.get(&discovery.name)?;
133                let (effect, visible) = model_skill_visibility(
134                    &policy,
135                    &projection.permission_overlay,
136                    grants.as_ref(),
137                    self.inner.store.cwd(),
138                    &discovery.name,
139                    skill,
140                );
141                let mut descriptor =
142                    skill_descriptor(skill, discovery.precedence_winner, effect, visible);
143                descriptor.location = discovery.path.to_string_lossy().into_owned();
144                descriptor.source = discovery.source;
145                Some(descriptor)
146            })
147            .collect::<Vec<_>>();
148        skills.sort_by(|left, right| {
149            left.name
150                .cmp(&right.name)
151                .then_with(|| left.location.cmp(&right.location))
152        });
153        Ok(SkillsListResult { skills })
154    }
155
156    pub fn get_skill(
157        &self,
158        session: SessionId,
159        name: &str,
160        args: &str,
161    ) -> Result<SkillsGetResult, EngineError> {
162        let projection = self.inner.store.get(session)?;
163        let policy = permissions::governing_agent_for_skills(&projection);
164        let skill = self.skill(name)?;
165        let effect = permissions::effective_permission_with_overlay(
166            &policy,
167            Some(&projection.permission_overlay),
168            PermissionAction::Skill,
169            name,
170            self.inner.store.cwd(),
171        )
172        .0;
173        if effect == PermissionEffect::Deny {
174            return Err(EngineError::Permission(format!("skill `{name}` is denied")));
175        }
176        Ok(SkillsGetResult {
177            skill: skill_descriptor(skill, true, effect, true),
178            rendered: render_skill(skill, args),
179        })
180    }
181
182    pub fn get_user_skill(
183        &self,
184        session: SessionId,
185        name: &str,
186        args: &str,
187    ) -> Result<SkillsGetResult, EngineError> {
188        let result = self.get_skill(session, name, args)?;
189        if !result.skill.user_invocable {
190            return Err(EngineError::Permission(format!(
191                "skill `{name}` is not user-invocable"
192            )));
193        }
194        Ok(result)
195    }
196
197    pub fn get_model_skill(
198        &self,
199        session: SessionId,
200        name: &str,
201        args: &str,
202    ) -> Result<SkillsGetResult, EngineError> {
203        let projection = self.inner.store.get(session)?;
204        let policy = permissions::governing_agent_for_skills(&projection);
205        let grants = self.skill_grants_for_session(session);
206        let visible = |skill_name: &str, skill: &SkillDocument| {
207            model_skill_visibility(
208                &policy,
209                &projection.permission_overlay,
210                grants.as_ref(),
211                self.inner.store.cwd(),
212                skill_name,
213                skill,
214            )
215            .1
216        };
217        let Some(skill) = self
218            .inner
219            .skills
220            .get(name)
221            .filter(|skill| visible(name, skill))
222        else {
223            let hints = self
224                .inner
225                .skills
226                .skills()
227                .filter(|(skill_name, skill)| visible(skill_name, skill))
228                .map(|(skill_name, _)| skill_name.as_str())
229                .collect::<Vec<_>>()
230                .join(", ");
231            return Err(EngineError::ToolFailed(format!(
232                "unknown or unavailable model skill `{name}`; valid skills: {hints}"
233            )));
234        };
235        let effect = model_skill_visibility(
236            &policy,
237            &projection.permission_overlay,
238            grants.as_ref(),
239            self.inner.store.cwd(),
240            name,
241            skill,
242        )
243        .0;
244        Ok(SkillsGetResult {
245            skill: skill_descriptor(skill, true, effect, true),
246            rendered: render_skill(skill, args),
247        })
248    }
249
250    pub async fn invoke_skill(
251        &self,
252        session: SessionId,
253        run: Option<cookie_agent_protocol::RunId>,
254        name: &str,
255        args: &str,
256        permission_preapproved: bool,
257    ) -> Result<SkillInvocation, EngineError> {
258        let projection = self.inner.store.get(session)?;
259        let policy = permissions::governing_agent_for_skills(&projection);
260        let skill = self.skill(name)?.clone();
261        if !permission_preapproved && !skill.frontmatter.user_invocable {
262            return Err(EngineError::Permission(format!(
263                "skill `{name}` is not user-invocable"
264            )));
265        }
266        let effect = permissions::effective_permission_with_overlay(
267            &policy,
268            Some(&projection.permission_overlay),
269            PermissionAction::Skill,
270            name,
271            self.inner.store.cwd(),
272        )
273        .0;
274        if !permission_preapproved && effect != PermissionEffect::Allow {
275            return Err(EngineError::Permission(format!(
276                "skill `{name}` requires {}",
277                if effect == PermissionEffect::Deny {
278                    "permission that is denied"
279                } else {
280                    "approval"
281                }
282            )));
283        }
284        let plan = prepared_skill_invocation(&skill, args);
285        self.install_prepared_skill(session, run, &plan).await
286    }
287
288    pub(super) async fn install_prepared_skill(
289        &self,
290        session: SessionId,
291        run: Option<RunId>,
292        plan: &PreparedSkillInvocation,
293    ) -> Result<SkillInvocation, EngineError> {
294        let projection = self.inner.store.get(session)?;
295        let name = &plan.name;
296        let duplicate = projection.log.event_snapshot().iter().any(|event| {
297            matches!(
298                &event.payload,
299                EventPayload::SkillLoaded { name: prior_name, rendered_body, .. }
300                    if prior_name == name && rendered_body.as_bytes() == plan.rendered.as_bytes()
301            )
302        });
303        let event = if duplicate {
304            EventPayload::SkillInvocationNoted { name: name.clone() }
305        } else {
306            EventPayload::SkillLoaded {
307                name: name.clone(),
308                rendered_body: plan.rendered.clone(),
309                source_path: plan.source_path.clone(),
310                args: plan.args.clone(),
311                base_dir: plan.base_dir.clone(),
312                supporting_files: plan.supporting_files.clone(),
313            }
314        };
315        self.append(session, run, super::event_origin("engine:skills"), event)
316            .await?;
317        let mut overlays = self
318            .inner
319            .skills_runtime
320            .grants
321            .lock()
322            .unwrap_or_else(|poisoned| poisoned.into_inner());
323        let per_skill = overlays.entry(session).or_default();
324        if plan.grants.is_empty() {
325            per_skill.remove(name);
326        } else {
327            per_skill.insert(
328                name.clone(),
329                SkillGrantOverlay {
330                    source: format!("skill:{name}"),
331                    overlay: SessionPermissionOverlay {
332                        rules: plan.grants.clone(),
333                    },
334                },
335            );
336        }
337        if per_skill.is_empty() {
338            overlays.remove(&session);
339        }
340        drop(overlays);
341        if let Some(model) = &plan.model {
342            self.inner
343                .skills_runtime
344                .models
345                .lock()
346                .unwrap_or_else(|poisoned| poisoned.into_inner())
347                .insert(session, model.clone());
348        }
349        Ok(SkillInvocation {
350            name: name.clone(),
351            rendered: plan.rendered.clone(),
352            context: plan.context,
353        })
354    }
355
356    pub fn skill_invocation_context(
357        &self,
358        name: &str,
359    ) -> Result<Option<SkillContext>, EngineError> {
360        Ok(self.skill(name)?.frontmatter.context)
361    }
362
363    pub fn is_direct_skill_call(&self, call_id: ToolCallId) -> bool {
364        self.inner
365            .skills_runtime
366            .direct_calls
367            .lock()
368            .unwrap_or_else(|poisoned| poisoned.into_inner())
369            .contains(&call_id)
370    }
371
372    #[cfg(test)]
373    pub(crate) fn stage_skill_fork_for_test(
374        &self,
375        call_id: ToolCallId,
376        payload: &cookie_agent_protocol::StagedSkillPayload,
377    ) {
378        self.inner
379            .skills_runtime
380            .pending_forks
381            .lock()
382            .expect("pending skill forks lock poisoned")
383            .insert(call_id, prepared_skill_from_payload(payload));
384    }
385
386    pub(super) async fn execute_direct_skill(
387        &self,
388        active: Arc<ActiveRun>,
389        run: RunId,
390        name: String,
391        args: String,
392    ) -> Result<(), EngineError> {
393        self.get_user_skill(active.session, &name, &args)?;
394        let call_id = ToolCallId::new_v7();
395        self.inner
396            .skills_runtime
397            .direct_calls
398            .lock()
399            .unwrap_or_else(|poisoned| poisoned.into_inner())
400            .insert(call_id);
401        let result = self
402            .execute_prepared_call(
403                active,
404                run,
405                ToolCall {
406                    id: call_id,
407                    name: "skill".into(),
408                    arguments: serde_json::json!({"name": name, "args": args}),
409                },
410            )
411            .await;
412        self.inner
413            .skills_runtime
414            .direct_calls
415            .lock()
416            .unwrap_or_else(|poisoned| poisoned.into_inner())
417            .remove(&call_id);
418        result.map(|_| ()).map_err(|error| {
419            if error.to_string().contains("tool_denied") {
420                EngineError::Permission(error.to_string())
421            } else {
422                error
423            }
424        })
425    }
426
427    pub async fn execute_skill_fork(
428        &self,
429        session: SessionId,
430        run: RunId,
431        call_id: ToolCallId,
432        name: &str,
433        args: &str,
434    ) -> Result<cookie_agent_protocol::PersistedToolResult, EngineError> {
435        let plan = self.prepare_skill_invocation(name, args)?;
436        let agent_type = self
437            .delegate_targets(session)?
438            .into_iter()
439            .next()
440            .ok_or_else(|| {
441                EngineError::ToolFailed("fork skill requires an available delegation target".into())
442            })?;
443        self.inner
444            .skills_runtime
445            .pending_forks
446            .lock()
447            .unwrap_or_else(|poisoned| poisoned.into_inner())
448            .insert(call_id, plan.clone());
449        let active = self
450            .inner
451            .sessions
452            .active
453            .lock()
454            .unwrap_or_else(|poisoned| poisoned.into_inner())
455            .get(&run)
456            .cloned()
457            .filter(|active| active.session == session)
458            .ok_or(EngineError::MissingRun(run))?;
459        let result = self
460            .execute_prepared_call(
461                active,
462                run,
463                ToolCall {
464                    id: call_id,
465                    name: "delegate_subagent".into(),
466                    arguments: serde_json::json!({
467                        "description": format!("Skill {name}"),
468                        "prompt": format!("Apply the staged skill `{name}`."),
469                        "agent_type": agent_type,
470                        "background": false,
471                        "resume_session_id": null,
472                        "inherit_context": false,
473                    }),
474                },
475            )
476            .await;
477        self.inner
478            .skills_runtime
479            .pending_forks
480            .lock()
481            .unwrap_or_else(|poisoned| poisoned.into_inner())
482            .remove(&call_id);
483        result
484    }
485
486    async fn execute_prepared_call(
487        &self,
488        active: Arc<ActiveRun>,
489        run: RunId,
490        call: ToolCall,
491    ) -> Result<cookie_agent_protocol::PersistedToolResult, EngineError> {
492        let binding = active
493            .policy
494            .selected_suffix
495            .first()
496            .ok_or(EngineError::NoRunnableModel)?;
497        let turn_context = Arc::new(TurnAgentContext {
498            agent: active.policy.agent.agent.clone(),
499            model: binding.selection.model.clone(),
500            adapter: crate::policy::wire_adapter(binding.descriptor.adapter_id.as_str()),
501            adapter_family: crate::policy::adapter_family(binding.descriptor.adapter_id.as_str()),
502            capabilities: active
503                .policy
504                .model_capabilities(binding)
505                .ok_or(EngineError::NoRunnableModel)?,
506        });
507        let prepared = self
508            .prepare_tool_call(
509                active.session,
510                run,
511                call,
512                &active.policy,
513                turn_context.clone(),
514            )
515            .await;
516        self.execute_tool(active, run, prepared, turn_context)
517            .await
518            .map_err(|failure| EngineError::ToolFailed(failure.message))
519    }
520
521    fn prepare_skill_invocation(
522        &self,
523        name: &str,
524        args: &str,
525    ) -> Result<PreparedSkillInvocation, EngineError> {
526        let skill = self.skill(name)?.clone();
527        Ok(prepared_skill_invocation(&skill, args))
528    }
529
530    pub(super) fn prepare_user_skill_invocation(
531        &self,
532        name: &str,
533        args: &str,
534    ) -> Result<PreparedSkillInvocation, EngineError> {
535        let skill = self.skill(name)?.clone();
536        if !skill.frontmatter.user_invocable {
537            return Err(EngineError::Permission(format!(
538                "skill `{name}` is not user-invocable"
539            )));
540        }
541        Ok(prepared_skill_invocation(&skill, args))
542    }
543
544    pub(crate) fn stage_child_skill_from_event(
545        &self,
546        session: SessionId,
547        payload: &cookie_agent_protocol::StagedSkillPayload,
548    ) {
549        self.inner
550            .skills_runtime
551            .pending_child
552            .lock()
553            .unwrap_or_else(|poisoned| poisoned.into_inner())
554            .insert(session, prepared_skill_from_payload(payload));
555    }
556
557    pub(super) fn pending_child_skill(
558        &self,
559        session: SessionId,
560    ) -> Option<PreparedSkillInvocation> {
561        self.inner
562            .skills_runtime
563            .pending_child
564            .lock()
565            .unwrap_or_else(|poisoned| poisoned.into_inner())
566            .get(&session)
567            .cloned()
568    }
569
570    pub(super) fn take_pending_child_skill(
571        &self,
572        session: SessionId,
573    ) -> Option<PreparedSkillInvocation> {
574        self.inner
575            .skills_runtime
576            .pending_child
577            .lock()
578            .unwrap_or_else(|poisoned| poisoned.into_inner())
579            .remove(&session)
580    }
581
582    pub(crate) fn transfer_pending_skill_fork(
583        &self,
584        call_id: ToolCallId,
585        _child_session: SessionId,
586    ) {
587        self.inner
588            .skills_runtime
589            .pending_forks
590            .lock()
591            .unwrap_or_else(|poisoned| poisoned.into_inner())
592            .remove(&call_id);
593    }
594
595    pub fn take_skill_model_override(
596        &self,
597        session: SessionId,
598    ) -> Option<cookie_agent_protocol::ModelKey> {
599        self.inner
600            .skills_runtime
601            .models
602            .lock()
603            .unwrap_or_else(|poisoned| poisoned.into_inner())
604            .remove(&session)
605    }
606
607    pub(crate) fn clear_skill_turn_state(&self, session: SessionId) {
608        self.inner
609            .skills_runtime
610            .grants
611            .lock()
612            .unwrap_or_else(|poisoned| poisoned.into_inner())
613            .remove(&session);
614        self.inner
615            .skills_runtime
616            .models
617            .lock()
618            .unwrap_or_else(|poisoned| poisoned.into_inner())
619            .remove(&session);
620    }
621
622    fn skill(&self, name: &str) -> Result<&SkillDocument, EngineError> {
623        self.inner.skills.get(name).ok_or_else(|| {
624            let names = self
625                .inner
626                .skills
627                .skills()
628                .map(|(name, _)| name.as_str())
629                .collect::<Vec<_>>()
630                .join(", ");
631            EngineError::ToolFailed(format!("unknown skill `{name}`; valid skills: {names}"))
632        })
633    }
634
635    pub(crate) fn compose_skill_listing(
636        &self,
637        session: SessionId,
638        policy: &mut FrozenRunPolicy,
639        prospective_grants: Option<&SessionPermissionOverlay>,
640    ) -> Result<(), EngineError> {
641        let projection = self.inner.store.get(session)?;
642        let visible = self
643            .inner
644            .skills
645            .skills()
646            .filter_map(|(name, skill)| {
647                model_skill_visibility(
648                    &policy.agent,
649                    &projection.permission_overlay,
650                    prospective_grants,
651                    self.inner.store.cwd(),
652                    name,
653                    skill,
654                )
655                .1
656                .then_some(skill)
657            })
658            .collect::<Vec<_>>();
659        if visible.is_empty() {
660            return Ok(());
661        }
662        let context_tokens = policy.selected_suffix[0]
663            .descriptor
664            .capabilities
665            .limits
666            .context
667            .unwrap_or(8_192);
668        let listing = cookie_agent_config::render_available_skills(visible, context_tokens)
669            .map_err(|error| EngineError::Config(Box::new(error)))?;
670        push_prompt_block(&mut policy.agent.composed_prompt, &listing);
671        policy.agent.prompt_fingerprint =
672            Sha256Digest::of_bytes(policy.agent.composed_prompt.as_bytes());
673        let mut document_fingerprint = policy
674            .agent
675            .document_fingerprint
676            .as_str()
677            .as_bytes()
678            .to_vec();
679        document_fingerprint.extend_from_slice(listing.as_bytes());
680        policy.agent.document_fingerprint = Sha256Digest::of_bytes(&document_fingerprint);
681        Ok(())
682    }
683}
684
685fn skill_descriptor(
686    skill: &SkillDocument,
687    precedence_winner: bool,
688    permission_effect: PermissionEffect,
689    visible: bool,
690) -> SkillDescriptor {
691    SkillDescriptor {
692        name: skill.frontmatter.name.clone(),
693        description: skill.frontmatter.description.clone(),
694        when_to_use: skill.frontmatter.when_to_use.clone(),
695        location: skill.path.to_string_lossy().into_owned(),
696        source: skill.source,
697        precedence_winner,
698        permission_effect,
699        visible,
700        user_invocable: skill.frontmatter.user_invocable,
701        argument_hint: skill.frontmatter.argument_hint.clone(),
702    }
703}
704
705fn model_skill_visibility(
706    policy: &cookie_agent_protocol::AgentSnapshot,
707    overlay: &SessionPermissionOverlay,
708    grants: Option<&SessionPermissionOverlay>,
709    workspace: &Path,
710    name: &str,
711    skill: &SkillDocument,
712) -> (PermissionEffect, bool) {
713    let effect = permissions::effective_permission_with_overlay(
714        policy,
715        Some(overlay),
716        PermissionAction::Skill,
717        name,
718        workspace,
719    )
720    .0;
721    let action_visible = permissions::PermissionPipeline::tool_visible_with_grants(
722        policy,
723        Some(overlay),
724        grants,
725        "skill",
726        workspace,
727    );
728    (
729        effect,
730        action_visible
731            && effect != PermissionEffect::Deny
732            && !skill.frontmatter.disable_model_invocation,
733    )
734}
735
736fn prepared_skill_invocation(skill: &SkillDocument, args: &str) -> PreparedSkillInvocation {
737    PreparedSkillInvocation {
738        name: skill.frontmatter.name.clone(),
739        args: args.to_owned(),
740        rendered: render_skill(skill, args),
741        source_path: skill.path.to_string_lossy().into_owned(),
742        base_dir: skill.base_dir.to_string_lossy().into_owned(),
743        supporting_files: skill
744            .supporting_files
745            .iter()
746            .map(|path| path.to_string_lossy().into_owned())
747            .collect(),
748        grants: skill
749            .frontmatter
750            .allowed_tools
751            .iter()
752            .map(|grant| PermissionRule {
753                action: grant.action,
754                resource: grant.pattern.clone(),
755                effect: PermissionEffect::Allow,
756            })
757            .collect(),
758        model: skill.frontmatter.model.clone(),
759        context: skill.frontmatter.context,
760    }
761}
762
763fn prepared_skill_from_payload(
764    payload: &cookie_agent_protocol::StagedSkillPayload,
765) -> PreparedSkillInvocation {
766    PreparedSkillInvocation {
767        name: payload.name.clone(),
768        args: payload.args.clone(),
769        rendered: payload.rendered_body.clone(),
770        source_path: payload.source_path.clone(),
771        base_dir: payload.base_dir.clone(),
772        supporting_files: payload.supporting_files.clone(),
773        grants: payload.grants.clone(),
774        model: payload.model.clone(),
775        context: Some(SkillContext::Fork),
776    }
777}
778
779fn render_skill(skill: &SkillDocument, args: &str) -> String {
780    let positional = args.split_whitespace().collect::<Vec<_>>();
781    let mut variables = BTreeMap::new();
782    variables.insert("ARGUMENTS".to_owned(), args.to_owned());
783    variables.insert(
784        "{COOKIE_SKILL_DIR}".to_owned(),
785        skill.base_dir.to_string_lossy().into_owned(),
786    );
787    for (index, value) in positional.into_iter().enumerate() {
788        variables.insert((index + 1).to_string(), value.to_owned());
789    }
790    let mut output = String::new();
791    let bytes = skill.body.as_bytes();
792    let mut index = 0;
793    while index < bytes.len() {
794        if bytes[index] == b'$' {
795            let rest = &skill.body[index + 1..];
796            let key = if rest.starts_with("{COOKIE_SKILL_DIR}") {
797                Some("{COOKIE_SKILL_DIR}")
798            } else if rest.starts_with("ARGUMENTS") {
799                Some("ARGUMENTS")
800            } else {
801                let digits = rest.bytes().take_while(u8::is_ascii_digit).count();
802                (digits > 0).then_some(&rest[..digits])
803            };
804            if let Some(key) = key {
805                output.push_str(variables.get(key).map_or("", String::as_str));
806                index += 1 + key.len();
807                continue;
808            }
809        }
810        let character = skill.body[index..]
811            .chars()
812            .next()
813            .expect("character boundary");
814        output.push(character);
815        index += character.len_utf8();
816    }
817    output
818}