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
57
# 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 = {}
  @tool_sources = {}     # { tool_name(String) => :core_tool | "skillset:<name>" } — Phase 1.5
  @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



329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
# File 'lib/kairos_mcp/tool_registry.rb', line 329

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.



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

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



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

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.



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

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



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

def lifecycle_hook_names
  @lifecycle_hooks.keys
end

#list_toolsObject



307
308
309
# File 'lib/kairos_mcp/tool_registry.rb', line 307

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.



313
314
315
316
317
318
319
320
321
# File 'lib/kairos_mcp/tool_registry.rb', line 313

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
  @tool_sources[name] = :dynamic_proxy
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.



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

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



292
293
294
295
296
297
298
299
300
301
# File 'lib/kairos_mcp/tool_registry.rb', line 292

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



270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
# File 'lib/kairos_mcp/tool_registry.rb', line 270

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|
      # Phase 1.5: thread source attribution for capability_status manifest
      register_if_defined(cls, source: "skillset:#{skillset.name}")
    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



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
264
265
266
267
# File 'lib/kairos_mcp/tool_registry.rb', line 184

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')

  # Phase 1.5 — Capability Boundary self-articulation
  register_if_defined('KairosMcp::Tools::CapabilityStatus')

  # 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



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

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

#unregister_tool(name) ⇒ Object

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



324
325
326
327
# File 'lib/kairos_mcp/tool_registry.rb', line 324

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