skill_registry.py
1 """Built-in OpenSandbox skill metadata and rendering helpers.""" 2 3 from __future__ import annotations 4 5 import importlib.resources 6 from dataclasses import dataclass 7 from pathlib import Path 8 9 10 @dataclass(frozen=True) 11 class SkillSpec: 12 """A bundled skill shipped with the CLI.""" 13 14 slug: str 15 package_file: str 16 title: str 17 summary: str 18 trigger_hint: str 19 marker_id: str 20 21 22 BUILTIN_SKILLS: dict[str, SkillSpec] = { 23 "sandbox-troubleshooting": SkillSpec( 24 slug="sandbox-troubleshooting", 25 package_file="opensandbox-sandbox-troubleshooting.md", 26 title="OpenSandbox Sandbox Troubleshooting", 27 summary=( 28 "Triage failed or unhealthy sandboxes with state, health, summary, " 29 "inspect, events, logs, and concrete remediation steps." 30 ), 31 trigger_hint=( 32 "Use when the user reports sandbox startup failures, crashes, OOM, " 33 "image pull problems, pending sandboxes, or unreachable services." 34 ), 35 marker_id="opensandbox-sandbox-troubleshooting", 36 ), 37 "sandbox-lifecycle": SkillSpec( 38 slug="sandbox-lifecycle", 39 package_file="opensandbox-sandbox-lifecycle.md", 40 title="OpenSandbox Sandbox Lifecycle", 41 summary=( 42 "Create, inspect, renew, pause, resume, and terminate sandboxes " 43 "with the right defaults and follow-up checks." 44 ), 45 trigger_hint=( 46 "Use when the user wants to create or manage a sandbox and needs " 47 "the exact OpenSandbox CLI/API flow." 48 ), 49 marker_id="opensandbox-sandbox-lifecycle", 50 ), 51 "command-execution": SkillSpec( 52 slug="command-execution", 53 package_file="opensandbox-command-execution.md", 54 title="OpenSandbox Command Execution", 55 summary=( 56 "Run foreground and background commands, inspect status/logs, and " 57 "use persistent sessions inside a sandbox." 58 ), 59 trigger_hint=( 60 "Use when the user wants to execute commands in a sandbox, collect " 61 "logs, interrupt work, or reuse a persistent shell session." 62 ), 63 marker_id="opensandbox-command-execution", 64 ), 65 "file-operations": SkillSpec( 66 slug="file-operations", 67 package_file="opensandbox-file-operations.md", 68 title="OpenSandbox File Operations", 69 summary=( 70 "Read, write, upload, download, search, replace, and manage files " 71 "inside a sandbox without hand-wavy shell advice." 72 ), 73 trigger_hint=( 74 "Use when the user needs file or directory manipulation inside an " 75 "OpenSandbox sandbox." 76 ), 77 marker_id="opensandbox-file-operations", 78 ), 79 "network-egress": SkillSpec( 80 slug="network-egress", 81 package_file="opensandbox-network-egress.md", 82 title="OpenSandbox Network Egress", 83 summary=( 84 "Inspect and patch sandbox runtime egress rules when the user " 85 "needs to allow or deny outbound domains." 86 ), 87 trigger_hint=( 88 "Use when the user needs to view or modify outbound network access " 89 "for a sandbox, or debug domain allow and deny behavior." 90 ), 91 marker_id="opensandbox-network-egress", 92 ), 93 } 94 95 DEFAULT_SKILL = "sandbox-troubleshooting" 96 97 98 def list_builtin_skills() -> list[SkillSpec]: 99 """Return bundled skills in stable declaration order.""" 100 return list(BUILTIN_SKILLS.values()) 101 102 103 def get_builtin_skill(slug: str) -> SkillSpec: 104 """Return a bundled skill definition.""" 105 return BUILTIN_SKILLS[slug] 106 107 108 def get_builtin_skill_source(skill: SkillSpec) -> Path: 109 """Locate the bundled skill file shipped with the CLI package.""" 110 pkg = importlib.resources.files("opensandbox_cli") / "skills" / skill.package_file 111 if hasattr(pkg, "__fspath__"): 112 return Path(str(pkg)) 113 with importlib.resources.as_file(pkg) as resolved: 114 return Path(resolved) 115 116 117 def read_skill_markdown(skill: SkillSpec) -> str: 118 """Load bundled skill markdown.""" 119 return get_builtin_skill_source(skill).read_text(encoding="utf-8") 120 121 122 def split_frontmatter(markdown: str) -> tuple[str | None, str]: 123 """Split markdown into YAML frontmatter and body.""" 124 if not markdown.startswith("---\n"): 125 return None, markdown 126 127 closing = markdown.find("\n---\n", 4) 128 if closing == -1: 129 return None, markdown 130 131 frontmatter = markdown[4:closing] 132 body = markdown[closing + 5 :] 133 return frontmatter, body 134 135 136 def extract_section(body: str, heading: str) -> str | None: 137 """Extract a markdown section by exact heading text.""" 138 lines = body.splitlines() 139 capture = False 140 capture_level = 0 141 captured: list[str] = [] 142 143 for line in lines: 144 if line.startswith("## ") or line.startswith("### "): 145 if capture: 146 if capture_level == 2 and line.startswith("## "): 147 break 148 if capture_level == 3 and (line.startswith("## ") or line.startswith("### ")): 149 break 150 if line == f"## {heading}": 151 capture = True 152 capture_level = 2 153 continue 154 if line == f"### {heading}": 155 capture = True 156 capture_level = 3 157 continue 158 if capture: 159 captured.append(line) 160 161 if not captured: 162 return None 163 164 return "\n".join(captured).strip() or None 165 166 167 def render_skill_for_target( 168 skill: SkillSpec, 169 markdown: str, 170 *, 171 preserve_frontmatter: bool, 172 ) -> str: 173 """Render skill content for the target tool.""" 174 frontmatter, body = split_frontmatter(markdown) 175 body = body.strip() 176 177 if preserve_frontmatter or not frontmatter: 178 return markdown.strip() + "\n" 179 180 return ( 181 f"# {skill.title}\n\n" 182 f"{skill.summary}\n\n" 183 f"{body}\n" 184 )