Skip to content
Kward Search API index

Class: Kward::Skills::Registry

Inherits:
Object
  • Object
show all
Defined in:
lib/kward/skills/registry.rb

Overview

Discovers Agent Skills and reads their bounded instruction resources.

Project sources are included only when the caller marks them trusted. If duplicate skill names exist, the first source in documented precedence order wins.

Defined Under Namespace

Classes: SkillCandidate, SkillSource

Instance Method Summary collapse

Constructor Details

#initialize(config_dir:, workspace_root:, project_skills_trusted:, skill_class:, max_file_bytes:, markdown_parser:, inside_directory:, warning_sink: nil, project_skill_paths: nil) ⇒ Registry

Returns a new instance of Registry.



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/kward/skills/registry.rb', line 19

def initialize(config_dir:, workspace_root:, project_skills_trusted:, skill_class:, max_file_bytes:, markdown_parser:, inside_directory:, warning_sink: nil, project_skill_paths: nil)
  @config_dir = config_dir
  @workspace_root = workspace_root
  @project_skills_trusted = project_skills_trusted
  @skill_class = skill_class
  @max_file_bytes = max_file_bytes
  @markdown_parser = markdown_parser
  @inside_directory = inside_directory
  @warning_sink = warning_sink
  @project_skill_paths = project_skill_paths&.map do |path|
    File.realpath(path)
  rescue SystemCallError
    File.expand_path(path)
  end
end

Instance Method Details

#project_skill_candidatesArray<SkillCandidate>

Returns project skill files without activating their instructions.

Returns:



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/kward/skills/registry.rb', line 39

def project_skill_candidates
  skill_sources.select { |source| source.scope == :project }.flat_map do |source|
    discover_source(source).filter_map do |path|
      SkillCandidate.new(
        path: path,
        root: source.root,
        label: source.label,
        scope: source.scope,
        digest: skill_digest(path)
      )
    rescue StandardError => e
      emit_warning "Warning: skipping Kward skill #{path}: #{e.message}"
      nil
    end
  end
end

#read_skill_file(name, relative_path = nil) ⇒ String

Reads a skill's main instructions or a bounded relative resource.

Absolute paths, paths escaping the skill folder, missing files, and files above the configured size limit return user-facing error strings.

Parameters:

  • name (String, #to_s)

    discovered skill name

  • relative_path (String, nil) (defaults to: nil)

    file relative to the skill directory; defaults to SKILL.md

Returns:

  • (String)

    skill content or an error message



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/kward/skills/registry.rb', line 91

def read_skill_file(name, relative_path = nil)
  skill = skills.find { |candidate| candidate.name == name.to_s }
  return "Error: unknown skill: #{name}" unless skill

  path = relative_path.to_s.empty? ? "SKILL.md" : relative_path.to_s
  return "Error: skill path must be relative" if Pathname.new(path).absolute?

  base = File.realpath(skill.folder)
  target = File.expand_path(path, base)
  real_target = File.realpath(target)
  unless @inside_directory.call(real_target, base)
    return "Error: skill path outside skill folder: #{path}"
  end
  return "Error: skill path is not a file: #{path}" unless File.file?(real_target)

  size = File.size(real_target)
  return "Error: skill file too large: #{path} (#{size} bytes)" if size > @max_file_bytes

  content = File.read(real_target)
  relative_path.to_s.empty? ? skill_content(skill, content) : content
rescue Errno::ENOENT
  "Error: skill file not found: #{path}"
rescue StandardError => e
  "Error: could not read skill file #{path}: #{e.message}"
end

#skillsArray<ConfigFiles::Skill>

Returns discovered, validated skills in precedence order.

Returns:



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/kward/skills/registry.rb', line 60

def skills
  seen = {}
  skill_sources.flat_map do |source|
    scan_source(source).filter_map do |path|
      skill = parse_skill(path)
      next unless skill

      if seen[skill.name]
        emit_warning "Warning: skipping duplicate Kward skill #{skill.name.inspect}: #{path}"
        next
      end

      seen[skill.name] = true
      skill
    end
  end
rescue StandardError => e
  emit_warning "Warning: skipping Kward skills: #{e.message}"
  []
end