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 Markdown, 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, prompt_renderer: 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, prompt_renderer: 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.
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
# File 'lib/little_ghost/skills/catalog.rb', line 50 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, prompt_renderer: 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) @prompt_renderer = prompt_renderer @framework_prompts = FrameworkPrompts.new validate_workspace_resource_root!(workspace, sandbox) @skills = load_skills validate_workspace_resource_aliases!(sandbox) end |
Instance Method Details
#discovery_prompt ⇒ Object
Produces the Markdown, metadata-only prompt used for discovery.
93 94 95 96 97 98 99 100 101 102 |
# File 'lib/little_ghost/skills/catalog.rb', line 93 def discovery_prompt return "" if @skills.empty? framework_prompt( "skills/discovery/instructions", skills: @skills.each_value.map do |skill| {name: skill.name, description: skill.description, location: skill.path} end ) end |
#each(&block) ⇒ Object
Yields each Skill in lookup order.
75 76 77 |
# File 'lib/little_ghost/skills/catalog.rb', line 75 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.
81 82 83 84 85 |
# File 'lib/little_ghost/skills/catalog.rb', line 81 def fetch(name) @skills.fetch(name.to_s) do raise ConfigurationError, framework_prompt("skills/feedback/unknown", name: name.to_s, available: names) end end |
#format(skill) ⇒ Object
Formats one Skill, including allowed tools, compatibility, and bounded resource paths.
130 131 132 133 134 135 136 137 138 139 |
# File 'lib/little_ghost/skills/catalog.rb', line 130 def format(skill) framework_prompt( "skills/tools/activate/content", instructions: skill.instructions, allowed_tools: skill.allowed_tools, compatibility: skill.compatibility, location: skill.path, resources: skill_resources(skill) ) 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.
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 |
# File 'lib/little_ghost/skills/catalog.rb', line 105 def tool catalog = self Tool.define( name: "skills", description: framework_prompt("skills/tools/activate/description"), input_schema: { type: "object", properties: { skill_name: { type: "string", description: framework_prompt("skills/tools/activate/inputs/name/description") } }, required: ["skill_name"], additionalProperties: false } ) do |input| catalog.format(catalog.fetch(input.fetch("skill_name"))) rescue ConfigurationError => error raise ToolError, error. end end |