Class: KairosMcp::TlsConfig
- Inherits:
-
Object
- Object
- KairosMcp::TlsConfig
- 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
-
#cert_path ⇒ Object
readonly
Returns the value of attribute cert_path.
-
#key_path ⇒ Object
readonly
Returns the value of attribute key_path.
Instance Method Summary collapse
-
#bind_uri(host, port) ⇒ Object
Build a Puma bind URI.
-
#certificate_not_after ⇒ Object
Expiry (not_after) of the configured certificate, or nil if it cannot be read/parsed.
-
#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.
- #enabled? ⇒ Boolean
-
#initialize(http_config, data_dir:, force_enabled: nil) ⇒ TlsConfig
constructor
A new instance of TlsConfig.
- #scheme ⇒ Object
-
#validate! ⇒ Object
Fail-closed validation.
Constructor Details
#initialize(http_config, data_dir:, force_enabled: nil) ⇒ TlsConfig
Returns a new instance of TlsConfig.
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_path ⇒ Object (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_path ⇒ Object (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_after ⇒ Object
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
41 42 43 |
# File 'lib/kairos_mcp/tls_config.rb', line 41 def enabled? @enabled end |
#scheme ⇒ Object
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.
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 |