Class: KairosMcp::TlsConfig

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

Overview

TlsConfig: resolves optional TLS settings for the HTTP transport.

Design intent (Prop 2, partial autopoiesis): transport encryption is an execution-substrate concern, not a self-referential core capability. KairosChain does NOT implement crypto here. It delegates to Puma/OpenSSL and only decides the bind scheme (tcp:// vs ssl://) from config. The only logic in this class is path resolution and fail-closed validation.

Config shape (under the 'http' key of skills/config.yml):

http:
tls:
  enabled: false
  cert: "storage/tls/cert.pem"
  key:  "storage/tls/key.pem"

Constant Summary collapse

DEFAULT_CERT =
'storage/tls/cert.pem'
DEFAULT_KEY =
'storage/tls/key.pem'

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(http_config, data_dir:, force_enabled: nil) ⇒ TlsConfig

Returns a new instance of TlsConfig.

Parameters:

  • http_config (Hash)

    the 'http' section of the loaded config

  • data_dir (String)

    base dir for resolving relative cert/key paths

  • force_enabled (Boolean, nil) (defaults to: nil)

    CLI override; nil = use config value



31
32
33
34
35
36
37
38
39
# File 'lib/kairos_mcp/tls_config.rb', line 31

def initialize(http_config, data_dir:, force_enabled: nil)
  tls = (http_config || {})['tls'] || {}
  @data_dir = data_dir
  @enabled = force_enabled.nil? ? (tls['enabled'] == true) : force_enabled
  # An empty-string path in config falls back to the default rather than
  # resolving to nil (which would crash the --gen-cert path).
  @cert_path = resolve(present_or(tls['cert'], DEFAULT_CERT))
  @key_path  = resolve(present_or(tls['key'], DEFAULT_KEY))
end

Instance Attribute Details

#cert_pathObject (readonly)

Returns the value of attribute cert_path.



26
27
28
# File 'lib/kairos_mcp/tls_config.rb', line 26

def cert_path
  @cert_path
end

#key_pathObject (readonly)

Returns the value of attribute key_path.



26
27
28
# File 'lib/kairos_mcp/tls_config.rb', line 26

def key_path
  @key_path
end

Instance Method Details

#bind_uri(host, port) ⇒ Object

Build a Puma bind URI. Encryption params are passed to Puma, which performs the TLS handshake via OpenSSL.



87
88
89
90
91
92
93
# File 'lib/kairos_mcp/tls_config.rb', line 87

def bind_uri(host, port)
  h = bracket_ipv6(host)
  return "tcp://#{h}:#{port}" unless @enabled

  query = URI.encode_www_form('key' => @key_path, 'cert' => @cert_path)
  "ssl://#{h}:#{port}?#{query}"
end

#certificate_not_afterObject

Expiry (not_after) of the configured certificate, or nil if it cannot be read/parsed. Used to surface silent-expiry breakage at startup.



101
102
103
104
105
106
107
# File 'lib/kairos_mcp/tls_config.rb', line 101

def certificate_not_after
  return nil unless @cert_path && File.exist?(@cert_path)

  OpenSSL::X509::Certificate.new(File.read(@cert_path)).not_after
rescue StandardError
  nil
end

#days_until_expiry(now = Time.now) ⇒ Object

Whole days until the certificate expires (negative if already expired), or nil if the cert cannot be read. now is injectable for testing.



111
112
113
114
115
116
# File 'lib/kairos_mcp/tls_config.rb', line 111

def days_until_expiry(now = Time.now)
  not_after = certificate_not_after
  return nil unless not_after

  ((not_after - now) / 86_400).floor
end

#enabled?Boolean

Returns:

  • (Boolean)


41
42
43
# File 'lib/kairos_mcp/tls_config.rb', line 41

def enabled?
  @enabled
end

#schemeObject



95
96
97
# File 'lib/kairos_mcp/tls_config.rb', line 95

def scheme
  @enabled ? 'https' : 'http'
end

#validate!Object

Fail-closed validation. When TLS is enabled, the cert and key must exist, be readable, and parse as valid material — otherwise abort rather than start plain HTTP or crash later inside Puma with an opaque error.

Raises:



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/kairos_mcp/tls_config.rb', line 48

def validate!
  return unless @enabled

  problems = []
  problems << "certificate is missing (#{@cert_path})" unless @cert_path && File.exist?(@cert_path)
  problems << "private key is missing (#{@key_path})" unless @key_path && File.exist?(@key_path)

  if problems.empty?
    problems << "certificate is not readable (#{@cert_path})" unless File.readable?(@cert_path)
    problems << "private key is not readable (#{@key_path})" unless File.readable?(@key_path)
  end

  if problems.empty?
    cert = safe_parse { OpenSSL::X509::Certificate.new(File.read(@cert_path)) }
    key  = safe_parse { OpenSSL::PKey.read(File.read(@key_path)) }
    problems << "certificate is not valid PEM (#{@cert_path})" if cert.nil?
    problems << "private key is not valid (#{@key_path})" if key.nil?
    # A parseable cert + parseable key that do not form a pair would pass
    # independent checks but fail opaquely inside Puma's SSL setup.
    if cert && key && !cert.check_private_key(key)
      problems << "certificate and private key do not match (#{@cert_path} / #{@key_path})"
    end
  end

  return if problems.empty?

  raise TlsConfigError, <<~MSG.strip
    TLS is enabled but its material is unusable:
      - #{problems.join("\n  - ")}

    Generate a self-signed certificate for single-operator use:
      kairos-chain --gen-cert

    Or point http.tls.cert / http.tls.key at a valid certificate.
  MSG
end