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
-
.aggregate_manifest(registry) ⇒ Hash
Aggregate harness_requirement declarations across all registered tools.
-
.cli_in_path?(name) ⇒ Boolean
which-style PATH check using only filesystem (no subprocess).
-
.cli_version(name) ⇒ Object
Get version of a CLI by spawning subprocess (only when probe_versions: true).
-
.compute_used_externals(manifest_entries, active_harness) ⇒ Object
Compute used_externals from declared manifest entries given active_harness.
-
.detect_harness ⇒ Hash
Returns active_harness detection result.
-
.normalize_requirement(value) ⇒ Object
Normalize a tool’s harness_requirement return value to canonical Hash form.
-
.reset! ⇒ Object
Test-only escape hatch.
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).
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.}", severity: :declaration_error } entries << { name: name, declared: false, source: source, tier: :unknown, declaration_error: e. } 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.
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_harness ⇒ Hash
Returns active_harness detection result. Cached at process boot.
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 |