Class: KairosMcp::ToolRegistry

Inherits:
Object
  • Object
show all
Defined in:
lib/kairos_mcp/tool_registry.rb

Defined Under Namespace

Classes: GateDeniedError

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(user_context: nil) ⇒ ToolRegistry

Returns a new instance of ToolRegistry.

Parameters:

  • user_context (Hash, nil) (defaults to: nil)

    Authenticated user info from HTTP mode



50
51
52
53
54
55
56
# File 'lib/kairos_mcp/tool_registry.rb', line 50

def initialize(user_context: nil)
  @safety = Safety.new
  @safety.set_user(user_context) if user_context
  @tools = {}
  @lifecycle_hooks = {}  # { hook_name(Symbol) => { skillset:, class_name: } }
  register_tools
end

Class Method Details

.clear_gates!Object

For testing only



43
44
45
# File 'lib/kairos_mcp/tool_registry.rb', line 43

def self.clear_gates!
  @gate_mutex.synchronize { @gates = {} }
end

.register_gate(name, &block) ⇒ Object

Register a named authorization gate. Gates are called before every tool invocation with (tool_name, arguments, safety). Raise GateDeniedError to deny access.



28
29
30
# File 'lib/kairos_mcp/tool_registry.rb', line 28

def self.register_gate(name, &block)
  @gate_mutex.synchronize { @gates[name.to_sym] = block }
end

.run_gates(tool_name, arguments, safety) ⇒ Object



36
37
38
39
40
# File 'lib/kairos_mcp/tool_registry.rb', line 36

def self.run_gates(tool_name, arguments, safety)
  @gate_mutex.synchronize { @gates.values.dup }.each do |gate|
    gate.call(tool_name, arguments, safety)
  end
end

.unregister_gate(name) ⇒ Object



32
33
34
# File 'lib/kairos_mcp/tool_registry.rb', line 32

def self.unregister_gate(name)
  @gate_mutex.synchronize { @gates.delete(name.to_sym) }
end

Instance Method Details

#call_tool(name, arguments, invocation_context: nil) ⇒ Object



322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# File 'lib/kairos_mcp/tool_registry.rb', line 322

def call_tool(name, arguments, invocation_context: nil)
  tool = @tools[name]
  unless tool
    raise "Tool not found: #{name}"
  end

  # Defense-in-depth: enforce invocation policy at the registry boundary.
  # This duplicates the check in BaseTool#invoke_tool so that direct
  # call_tool calls with a context also respect whitelist/blacklist.
  if invocation_context && !invocation_context.allowed?(name)
    raise InvocationContext::PolicyDeniedError,
          "Tool '#{name}' blocked by invocation policy at registry boundary"
  end

  self.class.run_gates(name, arguments, @safety)
  tool.call(arguments)
rescue GateDeniedError => e
  [{ type: 'text', text: JSON.pretty_generate({ error: 'forbidden', message: e.message }) }]
rescue InvocationContext::DepthExceededError, InvocationContext::PolicyDeniedError => e
  [{ type: 'text', text: JSON.pretty_generate({ error: 'invocation_denied', message: e.message }) }]
end

#find_lifecycle_hook(hook_name) ⇒ Object

Convenience: resolve the class and instantiate. Retained for tests and callers that do not need to distinguish lookup failures from constructor failures.



173
174
175
176
177
# File 'lib/kairos_mcp/tool_registry.rb', line 173

def find_lifecycle_hook(hook_name)
  klass = lifecycle_hook_class(hook_name)
  return nil unless klass
  instantiate_lifecycle_hook(klass)
end

#instantiate_lifecycle_hook(klass) ⇒ Object



136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/kairos_mcp/tool_registry.rb', line 136

def instantiate_lifecycle_hook(klass)
  instance = klass.new
  # `KairosMcp::LifecycleHook === instance` uses Module#=== — does
  # not call any method on `instance`, so it works on BasicObject.
  unless KairosMcp::LifecycleHook === instance
    class_label =
      (klass.name && !klass.name.empty?) ? klass.name : klass.inspect
    raise LifecycleHook::InstanceViolation,
          "#{class_label}.new returned #{safe_inspect(instance)} " \
          "(#{safe_class_name(instance)}) which does not include " \
          'KairosMcp::LifecycleHook'
  end
  instance
end

#lifecycle_hook_class(hook_name) ⇒ Object

Resolve a registered hook to its Class (without instantiating). Returns nil if no SkillSet declared the hook. Raises ‘LifecycleHook::UnknownClass` if the registered class name cannot be constantized or does not include `LifecycleHook`.

R8→R9 (3-voice: Codex P1 / 4.6 P2 / 4.7 P2): split class-resolution from instantiation so bin/ can ‘.new` under a precise rescue. The broad `rescue StandardError` in the entrypoint otherwise mislabels any registry-logic bug as an instantiation failure.



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/kairos_mcp/tool_registry.rb', line 96

def lifecycle_hook_class(hook_name)
  entry = @lifecycle_hooks[hook_name.to_sym]
  return nil unless entry
  begin
    klass = Object.const_get(entry[:class_name])
  rescue NameError => e
    raise LifecycleHook::UnknownClass,
          "lifecycle hook class '#{entry[:class_name]}' is not defined " \
          "(declared by '#{entry[:skillset]}'): #{e.message}"
  end
  unless klass.is_a?(Class) && klass.include?(KairosMcp::LifecycleHook)
    raise LifecycleHook::UnknownClass,
          "class '#{entry[:class_name]}' does not include KairosMcp::LifecycleHook"
  end
  klass
end

#lifecycle_hook_namesObject



179
180
181
# File 'lib/kairos_mcp/tool_registry.rb', line 179

def lifecycle_hook_names
  @lifecycle_hooks.keys
end

#list_toolsObject



302
303
304
# File 'lib/kairos_mcp/tool_registry.rb', line 302

def list_tools
  @tools.values.map(&:to_schema)
end

#register_dynamic_tool(tool_instance) ⇒ Object

Register a pre-built tool instance (e.g., proxy tools from mcp_client). Cannot overwrite local (non-proxy) tools to prevent accidental replacement.



308
309
310
311
312
313
314
315
# File 'lib/kairos_mcp/tool_registry.rb', line 308

def register_dynamic_tool(tool_instance)
  name = tool_instance.name
  existing = @tools[name]
  if existing && !existing.respond_to?(:remote_name)
    raise "Cannot override local tool '#{name}' with dynamic registration"
  end
  @tools[name] = tool_instance
end

#register_lifecycle_hook(hook_name, class_name, skillset_name:) ⇒ Object

24/7 v0.4 §2.3 — LifecycleHook registry.

Register a hook declaration from a SkillSet. Conflicts (same hook name claimed by two SkillSets) raise LifecycleHook::Conflict — the Bootstrap layer refuses to silently pick a winner.



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/kairos_mcp/tool_registry.rb', line 63

def register_lifecycle_hook(hook_name, class_name, skillset_name:)
  key = hook_name.to_sym
  # R1 P1 (2-voice security): validate class name + enforce namespace
  # allowlist before trusting any skillset-sourced class identifier.
  validated = LifecycleHook.validate_class_name!(class_name)

  existing = @lifecycle_hooks[key]
  if existing && existing[:skillset] != skillset_name
    raise LifecycleHook::Conflict,
          "LifecycleHook '#{hook_name}' claimed by both " \
          "'#{existing[:skillset]}' and '#{skillset_name}'"
  end
  # R1 P2 (3-voice): same-skillset re-registration must not silently
  # overwrite with a DIFFERENT class. Same-class re-registration is a
  # harmless idempotent load (tests, reload).
  if existing && existing[:class_name] != validated
    raise LifecycleHook::Conflict,
          "LifecycleHook '#{hook_name}' re-registered by " \
          "'#{skillset_name}' with different class " \
          "('#{existing[:class_name]}' → '#{validated}')"
  end
  @lifecycle_hooks[key] = { skillset: skillset_name, class_name: validated }
end

#register_skill_toolsObject

Register tools defined in kairos.rb via tool block



287
288
289
290
291
292
293
294
295
296
# File 'lib/kairos_mcp/tool_registry.rb', line 287

def register_skill_tools
  require_relative 'skill_tool_adapter'
  require_relative 'kairos'

  Kairos.skills.each do |skill|
    next unless skill.has_tool?  # Only skills with tool block and executor
    adapter = SkillToolAdapter.new(skill, @safety, registry: self)
    register(adapter)
  end
end

#register_skillset_toolsObject

Register tools from enabled SkillSets



266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# File 'lib/kairos_mcp/tool_registry.rb', line 266

def register_skillset_tools
  require_relative 'skillset_manager'

  manager = SkillSetManager.new
  manager.enabled_skillsets.each do |skillset|
    skillset.load!
    skillset.tool_class_names.each do |cls|
      register_if_defined(cls)
    end
    # 24/7 v0.4 §2.3 — register lifecycle hooks declared by this SkillSet.
    skillset.lifecycle_hooks.each do |hook_name, class_name|
      register_lifecycle_hook(hook_name, class_name, skillset_name: skillset.name)
    end
  end
rescue LifecycleHook::Conflict
  raise  # never swallow — Bootstrap integrity depends on detection
rescue StandardError => e
  warn "[ToolRegistry] Failed to load SkillSet tools: #{e.message}"
end

#register_toolsObject



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'lib/kairos_mcp/tool_registry.rb', line 183

def register_tools
  # Load all tool files
  Dir[File.join(__dir__, 'tools', '*.rb')].each do |file|
    require file
  end

  # Register tools
  register_if_defined('KairosMcp::Tools::HelloWorld')
  
  # L0-A: skills/kairos.md (read-only)
  register_if_defined('KairosMcp::Tools::SkillsList')
  register_if_defined('KairosMcp::Tools::SkillsGet')
  
  # L0-B: skills/kairos.rb (self-modifying with full blockchain record)
  register_if_defined('KairosMcp::Tools::SkillsDslList')
  register_if_defined('KairosMcp::Tools::SkillsDslGet')
  register_if_defined('KairosMcp::Tools::SkillsEvolve')
  register_if_defined('KairosMcp::Tools::SkillsRollback')
  
  # Cross-layer promotion with optional Persona Assembly
  register_if_defined('KairosMcp::Tools::SkillsPromote')
  
  # Audit tools (health checks, archive management, recommendations)
  register_if_defined('KairosMcp::Tools::SkillsAudit')

  # L0: Instructions management (system prompt control with full blockchain record)
  register_if_defined('KairosMcp::Tools::InstructionsUpdate')
  
  # Resource tools (unified access to L0/L1/L2 resources)
  register_if_defined('KairosMcp::Tools::ResourceList')
  register_if_defined('KairosMcp::Tools::ResourceRead')

  # L1: knowledge/ (Anthropic skills format with hash-only blockchain record)
  register_if_defined('KairosMcp::Tools::KnowledgeList')
  register_if_defined('KairosMcp::Tools::KnowledgeGet')
  register_if_defined('KairosMcp::Tools::KnowledgeUpdate')
  
  # L2: context/ (Anthropic skills format without blockchain record)
  register_if_defined('KairosMcp::Tools::ContextSave')
  register_if_defined('KairosMcp::Tools::ContextCreateSubdir')
  
  # Chain tools
  register_if_defined('KairosMcp::Tools::ChainStatus')
  register_if_defined('KairosMcp::Tools::ChainRecord')
  register_if_defined('KairosMcp::Tools::ChainVerify')
  register_if_defined('KairosMcp::Tools::ChainHistory')
  register_if_defined('KairosMcp::Tools::ChainExport')
  register_if_defined('KairosMcp::Tools::ChainImport')

  # Formalization tools (DSL/AST partial formalization records)
  register_if_defined('KairosMcp::Tools::FormalizationRecord')
  register_if_defined('KairosMcp::Tools::FormalizationHistory')

  # Definition analysis tools (Phase 2: verification, decompilation, drift detection)
  register_if_defined('KairosMcp::Tools::DefinitionVerify')
  register_if_defined('KairosMcp::Tools::DefinitionDecompile')
  register_if_defined('KairosMcp::Tools::DefinitionDrift')

  # State commit tools (auditability)
  register_if_defined('KairosMcp::Tools::StateCommit')
  register_if_defined('KairosMcp::Tools::StateStatus')
  register_if_defined('KairosMcp::Tools::StateHistory')

  # Guide tools (discovery, help, metadata management)
  register_if_defined('KairosMcp::Tools::ToolGuide')

  # Token management (HTTP authentication)
  register_if_defined('KairosMcp::Tools::TokenManage')

  # System management tools (upgrade, migration)
  register_if_defined('KairosMcp::Tools::SystemUpgrade')

  # SkillSet-based tools (opt-in plugins from .kairos/skillsets/)
  register_skillset_tools

  # Skill-based tools (from kairos.rb with tool block)
  register_skill_tools if skill_tools_enabled?

  # Restore dynamic proxy tools from active mcp_client connections (Phase 4)
  restore_dynamic_tools
end

#set_workspace(roots) ⇒ Object



298
299
300
# File 'lib/kairos_mcp/tool_registry.rb', line 298

def set_workspace(roots)
  @safety.set_workspace(roots)
end

#unregister_tool(name) ⇒ Object

Remove a dynamically registered tool (e.g., on mcp_disconnect).



318
319
320
# File 'lib/kairos_mcp/tool_registry.rb', line 318

def unregister_tool(name)
  @tools.delete(name)
end