Class: LittleGhost::Skills::Catalog
- Inherits:
-
Object
- Object
- LittleGhost::Skills::Catalog
- Includes:
- Enumerable
- Defined in:
- lib/little_ghost/skills/catalog.rb
Overview
A Catalog lets an agent discover focused instructions without putting every skill in its prompt. The model sees short descriptions first and can load a skill's full instructions when the task calls for them.
catalog = LittleGhost::Skills::Catalog.new(paths: ["app/skills"])
catalog.names # => ["refund_policy", "search_orders"]
catalog.discovery_prompt.include?("refund_policy") # => true
catalog.tool # a LittleGhost::Tool that loads full instructions on demand
Each immediate child directory may contain one SKILL.md with YAML front
matter. Symbolic-link escapes, unsafe names, oversized files, and invalid
YAML are rejected or skipped before instructions reach a model. Optional
resource listings are limited by count and depth.
Choosing skill sources
Skill files become model instructions, so keep configured roots under
application control and non-user-writable. The allowed-tools field tells
the model what a skill expects; the Agent's Tool list and each Tool's
application checks still decide what can run. For a workspace://
resource root, the Catalog verifies the named read-only grant and rejects
direct writable aliases it can identify. LittleGhost cannot identify every
alias created by an outer container or mount namespace, so the application
must not expose the same files through another writable bind mount.
Constant Summary collapse
- DEFAULT_MAX_SKILLS =
:nodoc:
1_000- DEFAULT_MAX_FILE_BYTES =
:nodoc:
1_000_000- DEFAULT_MAX_RESOURCE_FILES =
:nodoc:
20- MAX_RESOURCE_DEPTH =
:nodoc:
3- RESOURCE_DIRECTORIES =
:nodoc:
%w[scripts references assets].freeze
- SAFE_NAME_PATTERN =
:nodoc:
/\A[a-zA-Z0-9_-]+\z/
Instance Method Summary collapse
-
#discovery_prompt ⇒ Object
Produces the escaped, metadata-only prompt used for discovery.
-
#each(&block) ⇒ Object
Yields each Skill in lookup order.
-
#fetch(name) ⇒ Object
Finds the named Skill.
-
#format(skill) ⇒ Object
Formats one Skill, including allowed tools, compatibility, and bounded resource paths.
-
#initialize(paths:, max_skills: DEFAULT_MAX_SKILLS, max_file_bytes: DEFAULT_MAX_FILE_BYTES, max_resource_files: DEFAULT_MAX_RESOURCE_FILES, only: nil, resource_root: nil, workspace: nil, sandbox: nil) ⇒ Catalog
constructor
Loads valid skills immediately using the supplied safety limits.
-
#names ⇒ Object
Lists immutable skill names in lookup order.
-
#tool ⇒ Object
Exposes full instructions on demand through a
skillsTool.
Constructor Details
#initialize(paths:, max_skills: DEFAULT_MAX_SKILLS, max_file_bytes: DEFAULT_MAX_FILE_BYTES, max_resource_files: DEFAULT_MAX_RESOURCE_FILES, only: nil, resource_root: nil, workspace: nil, sandbox: nil) ⇒ Catalog
Loads valid skills immediately using the supplied safety limits.
resource_root may be an absolute process-visible path. A
workspace://name reference also requires workspace and
sandbox; it must resolve to every configured skill root through a
read-only file-tool grant.
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
# File 'lib/little_ghost/skills/catalog.rb', line 51 def initialize( paths:, max_skills: DEFAULT_MAX_SKILLS, max_file_bytes: DEFAULT_MAX_FILE_BYTES, max_resource_files: DEFAULT_MAX_RESOURCE_FILES, only: nil, resource_root: nil, workspace: nil, sandbox: nil ) @paths = PathSet.new(paths) @max_skills = positive_integer(max_skills, :max_skills) @max_file_bytes = positive_integer(max_file_bytes, :max_file_bytes) @max_resource_files = positive_integer(max_resource_files, :max_resource_files) @only = Array(only).map(&:to_s).freeze if only @resource_root = ResourceRoot.normalize(resource_root) validate_workspace_resource_root!(workspace, sandbox) @skills = load_skills validate_workspace_resource_aliases!(sandbox) end |
Instance Method Details
#discovery_prompt ⇒ Object
Produces the escaped, metadata-only prompt used for discovery.
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
# File 'lib/little_ghost/skills/catalog.rb', line 93 def discovery_prompt return "" if @skills.empty? lines = ["<available_skills>"] @skills.each_value do |skill| lines.concat([ "<skill>", "<name>#{ERB::Util.html_escape(skill.name)}</name>", "<description>#{ERB::Util.html_escape(skill.description)}</description>", "<location>#{ERB::Util.html_escape(skill.path)}</location>", "</skill>" ]) end lines << "</available_skills>" lines.join("\n") end |
#each(&block) ⇒ Object
Yields each Skill in lookup order.
73 74 75 |
# File 'lib/little_ghost/skills/catalog.rb', line 73 def each(&block) @skills.each_value(&block) end |
#fetch(name) ⇒ Object
Finds the named Skill. When no Skill matches, the ConfigurationError names the available skills so the caller can correct the lookup.
79 80 81 82 83 84 85 |
# File 'lib/little_ghost/skills/catalog.rb', line 79 def fetch(name) @skills.fetch(name.to_s) do = "Unknown skill: #{name}" += ". Available skills: #{names.join(", ")}" unless @skills.empty? raise ConfigurationError, end end |
#format(skill) ⇒ Object
Formats one Skill, including allowed tools, compatibility, and bounded resource paths.
141 142 143 144 145 146 147 148 149 150 151 152 153 |
# File 'lib/little_ghost/skills/catalog.rb', line 141 def format(skill) parts = [skill.instructions] = [] << "Allowed tools: #{skill.allowed_tools.join(", ")}" unless skill.allowed_tools.empty? << "Compatibility: #{skill.compatibility}" if skill.compatibility << "Location: #{skill.path}" parts << "\n---\n#{.join("\n")}" unless .empty? resources = skill_resources(skill) unless resources.empty? parts << "\nAvailable resources:\n#{resources.map { |path| " #{path}" }.join("\n")}" end parts.join("\n") end |
#names ⇒ Object
Lists immutable skill names in lookup order.
88 89 90 |
# File 'lib/little_ghost/skills/catalog.rb', line 88 def names @skills.keys.freeze end |
#tool ⇒ Object
Exposes full instructions on demand through a skills Tool.
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
# File 'lib/little_ghost/skills/catalog.rb', line 111 def tool catalog = self Tool.define( name: "skills", description: <<~DESCRIPTION.strip, Activate a skill to load its full instructions. Use this tool to load the complete instructions for a skill listed in the available_skills section of your system prompt. DESCRIPTION input_schema: { type: "object", properties: { skill_name: { type: "string", description: "Exact name of one skill from available_skills. Pass only the bare name here; keep arguments and surrounding instructions in the task request." } }, required: ["skill_name"], additionalProperties: false } ) do |input| catalog.format(catalog.fetch(input.fetch("skill_name"))) rescue ConfigurationError => error raise ToolError, error. end end |