Module: KairosMcp::Capability

Defined in:
lib/kairos_mcp/capability.rb

Overview

Capability module — Phase 1.5 self-articulation infrastructure.

Design reference: docs/drafts/capability_boundary_design_v1.1.md

Provides:

  • active_harness detection (env_var first, auto-detect fallback, :unknown honest)

  • harness_requirement metadata normalization

  • manifest aggregation across BaseTool subclasses

8 invariants govern this module:

1. Self-articulation     — boundary must be queryable at runtime
2. Honest unknown        — :unknown beats false guess
3. Declare-not-enforce   — articulation only, no runtime gate
4. Structural congruence — DSL matches existing BaseTool method override pattern
5. Composability         — SkillSet tools participate equally
6. Active vs external separation (with same-source exclusion)
7. Forward-only metadata — opt-in, with declared:true/false in manifest
8. Acknowledgment         — runtime dependence is articulated, not silently absorbed

Constant Summary collapse

TIERS =
%i[core harness_assisted harness_specific].freeze
SAME_SOURCE_CLI =

Mapping from active_harness symbol to its “same-source” CLI name. When active_harness=:claude_code and a tool declares requires_externals: [:claude_cli], claude_cli is excluded from used_externals because it is the SAME source as the harness running KairosChain (active vs external separation invariant).

{
  claude_code: :claude_cli,
  codex_cli:   :codex_cli,
  cursor:      :cursor_cli
}.freeze

Class Method Summary collapse

Class Method Details

.aggregate_manifest(registry) ⇒ Hash

Aggregate harness_requirement declarations across all registered tools. Skip + warn on per-tool validation failure (partial-failure policy).

Parameters:

Returns:

  • (Hash)

    { tools: […], summary: …, declaration_errors: […] }



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/kairos_mcp/capability.rb', line 68

def aggregate_manifest(registry)
  tools_index = registry.instance_variable_get(:@tools) || {}
  sources = registry.instance_variable_get(:@tool_sources) || {}

  entries = []
  errors = []
  summary = Hash.new(0)

  tools_index.each do |name, tool|
    source = sources[name] || :core_tool
    # declared = explicitly overridden in tool subclass (vs inherited BaseTool default)
    declared = tool.method(:harness_requirement).owner != KairosMcp::Tools::BaseTool
    raw = safe_call_requirement(tool)

    begin
      normalized = normalize_requirement(raw)
      entry = { name: name, declared: declared, source: source }.merge(normalized)
      entries << entry
      tier_key = declared ? normalized[:tier] : :"undeclared_default_#{normalized[:tier]}"
      summary[tier_key] += 1
    rescue ArgumentError => e
      errors << { tool: name, issue: "invalid harness_requirement: #{e.message}",
                  severity: :declaration_error }
      entries << { name: name, declared: false, source: source, tier: :unknown,
                   declaration_error: e.message }
      summary[:declaration_errors] += 1
    end
  end

  { tools: entries, summary: summary.transform_values(&:to_i),
    declaration_errors: errors }
end

.cli_in_path?(name) ⇒ Boolean

which-style PATH check using only filesystem (no subprocess). Returns true/false.

Returns:

  • (Boolean)


103
104
105
106
107
108
109
110
# File 'lib/kairos_mcp/capability.rb', line 103

def cli_in_path?(name)
  return false unless name.is_a?(Symbol) || name.is_a?(String)
  bin = name.to_s
  ENV.fetch('PATH', '').split(File::PATH_SEPARATOR).any? do |dir|
    full = File.join(dir, bin)
    File.executable?(full) && !File.directory?(full)
  end
end

.cli_version(name) ⇒ Object

Get version of a CLI by spawning subprocess (only when probe_versions: true). Returns nil on any failure (Honest unknown).



114
115
116
117
118
119
120
# File 'lib/kairos_mcp/capability.rb', line 114

def cli_version(name)
  return nil unless cli_in_path?(name)
  out = `#{name} --version 2>&1`.strip
  $?.success? ? out.lines.first&.strip : nil
rescue StandardError
  nil
end

.compute_used_externals(manifest_entries, active_harness) ⇒ Object

Compute used_externals from declared manifest entries given active_harness. Applies same-source exclusion rule.



124
125
126
127
128
129
130
131
132
# File 'lib/kairos_mcp/capability.rb', line 124

def compute_used_externals(manifest_entries, active_harness)
  same_source = SAME_SOURCE_CLI[active_harness]
  union = manifest_entries.flat_map { |e| Array(e[:requires_externals]) }.uniq
  excluded = same_source && union.include?(same_source) ? [same_source] : []
  {
    value: union - excluded,
    same_source_excluded: excluded
  }
end

.detect_harnessHash

Returns active_harness detection result. Cached at process boot.

Returns:

  • (Hash)

    { active_harness:, detection_method:, confidence: }



39
40
41
# File 'lib/kairos_mcp/capability.rb', line 39

def detect_harness
  @detection ||= compute_detection
end

.normalize_requirement(value) ⇒ Object

Normalize a tool’s harness_requirement return value to canonical Hash form. Symbol → { tier: <symbol> } Hash → validated Hash (raises ArgumentError on violation)



51
52
53
54
55
56
57
58
59
60
61
# File 'lib/kairos_mcp/capability.rb', line 51

def normalize_requirement(value)
  hash = case value
         when Symbol then { tier: value }
         when Hash   then deep_symbolize(value)
         else
           raise ArgumentError, "harness_requirement must be Symbol or Hash, got #{value.class}"
         end

  validate!(hash)
  hash
end

.reset!Object

Test-only escape hatch. Production code never calls this.



44
45
46
# File 'lib/kairos_mcp/capability.rb', line 44

def reset!
  @detection = nil
end