/ cli / src / opensandbox_cli / skill_registry.py
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      )