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 or raises ConfigurationError.
-
#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.
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
# File 'lib/little_ghost/skills/catalog.rb', line 88 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 or raises ConfigurationError.
78 79 80 |
# File 'lib/little_ghost/skills/catalog.rb', line 78 def fetch(name) @skills.fetch(name.to_s) { raise ConfigurationError, "Unknown skill: #{name}" } end |
#format(skill) ⇒ Object
Formats one Skill, including allowed tools, compatibility, and bounded resource paths.
131 132 133 134 135 136 137 138 139 140 141 142 143 |
# File 'lib/little_ghost/skills/catalog.rb', line 131 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.
83 84 85 |
# File 'lib/little_ghost/skills/catalog.rb', line 83 def names @skills.keys.freeze end |
#tool ⇒ Object
Exposes full instructions on demand through a skills Tool.
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/little_ghost/skills/catalog.rb', line 106 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: "Name of the skill to activate."}}, required: ["skill_name"], additionalProperties: false } ) do |input| catalog.format(catalog.fetch(input.fetch("skill_name"))) rescue ConfigurationError => error raise ToolError, error. end end |