kairos-chain 3.39.0 → 3.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -0
- data/bin/kairos-chain +90 -4
- data/lib/kairos_mcp/http_server.rb +106 -5
- data/lib/kairos_mcp/tls_cert_generator.rb +113 -0
- data/lib/kairos_mcp/tls_config.rb +149 -0
- data/lib/kairos_mcp/version.rb +1 -1
- data/templates/knowledge/kairoschain_setup/kairoschain_setup.md +37 -4
- data/templates/knowledge/kairoschain_setup_jp/kairoschain_setup_jp.md +30 -4
- data/templates/skills/config.yml +15 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2285b28e2f2954ca10f6da869b7446b4d18fdc0f9174451f20ec8f523142b322
|
|
4
|
+
data.tar.gz: 9a14c47fd141de6ff7eaec10d6e56c44f01e96b1cb0b9ab962eedb4baccc8e4b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 89a5a8a47b38f8f703283c58d9e402efeab1a2124fa1ad2195c0820edd985301ce68b22dd40012614499d09f9b34410905e473ca33ccad97e8d32d804ba46eb1
|
|
7
|
+
data.tar.gz: df15f0d5f6cdb22039840cf780b235cba8194e77da2738478b37dbeb446c25814609887083566388bae2abeb2f067e0aa2ecb24d16b4ca626b75644818d794eb
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,37 @@ All notable changes to the `kairos-chain` gem will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
This project follows [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [3.40.0] - 2026-07-06
|
|
8
|
+
|
|
9
|
+
### Added — Optional built-in TLS/HTTPS for the HTTP transport
|
|
10
|
+
|
|
11
|
+
Opt-in TLS termination for the Puma-based MCP HTTP server, so a self-hosted
|
|
12
|
+
instance can be reached securely over the network without a reverse proxy.
|
|
13
|
+
Encryption is delegated to Puma/OpenSSL (Prop 2: transport is execution
|
|
14
|
+
substrate); KairosChain only selects the bind scheme and can generate a
|
|
15
|
+
self-signed certificate for single-operator use. Default remains plain HTTP
|
|
16
|
+
(backward compatible).
|
|
17
|
+
|
|
18
|
+
- **`TlsConfig`** — `tcp://` vs `ssl://` bind selection, fail-closed validation
|
|
19
|
+
(exists + readable + valid PEM + cert/key pair match), IPv6 host bracketing,
|
|
20
|
+
certificate-expiry reporting.
|
|
21
|
+
- **`TlsCertGenerator`** — self-signed leaf certificate via the OpenSSL stdlib:
|
|
22
|
+
`CA:FALSE`, `keyUsage` + `extendedKeyUsage=serverAuth`, SAN built from a host
|
|
23
|
+
list (loopback + system hostname + config host + `--cert-host`), positive
|
|
24
|
+
serial, atomic `0600` private-key write.
|
|
25
|
+
- **`HttpServer`** — `--tls` bind, startup certificate-expiry warning, and
|
|
26
|
+
fail-closed refusal to start an unauthenticated endpoint on a network-reachable
|
|
27
|
+
bind (override with `KAIROS_ALLOW_OPEN_ENDPOINT=1`).
|
|
28
|
+
- **CLI** — `--tls`, `--gen-cert`, `--cert-host`; `--init-admin` sample URL
|
|
29
|
+
scheme resolved from config + flag.
|
|
30
|
+
- **Config** — `http.tls` block (default disabled).
|
|
31
|
+
- **Docs** — `kairoschain_setup` knowledge (en/jp) documents built-in TLS
|
|
32
|
+
(Option A) alongside the reverse-proxy path (Option B).
|
|
33
|
+
|
|
34
|
+
Reviewed via a 2-round multi-LLM review (R1 raised 3 P1 + P2s; R2 verified all
|
|
35
|
+
resolved). 36/36 HTTP tests pass; verified end-to-end with a real
|
|
36
|
+
certificate-verified HTTPS handshake and the fail-closed guard matrix.
|
|
37
|
+
|
|
7
38
|
## [3.39.0] - 2026-07-05
|
|
8
39
|
|
|
9
40
|
### Added — L1 `multi_llm_review_workflow` v3.5 → v3.6: Step 0.25 Unknowns Pass
|
data/bin/kairos-chain
CHANGED
|
@@ -636,10 +636,22 @@ OptionParser.new do |opts|
|
|
|
636
636
|
options[:port] = port
|
|
637
637
|
end
|
|
638
638
|
|
|
639
|
-
opts.on('--host HOST', 'HTTP bind host (default:
|
|
639
|
+
opts.on('--host HOST', 'HTTP bind host (default: config http.host, else 127.0.0.1)') do |host|
|
|
640
640
|
options[:host] = host
|
|
641
641
|
end
|
|
642
642
|
|
|
643
|
+
opts.on('--tls', 'Enable TLS (HTTPS) for --http mode (overrides config)') do
|
|
644
|
+
options[:tls] = true
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
opts.on('--gen-cert', 'Generate a self-signed TLS cert/key for --tls and exit') do
|
|
648
|
+
options[:gen_cert] = true
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
opts.on('--cert-host HOST', 'Add a hostname/IP to the generated cert SAN (repeatable)') do |h|
|
|
652
|
+
(options[:cert_hosts] ||= []) << h
|
|
653
|
+
end
|
|
654
|
+
|
|
643
655
|
opts.on('--init-admin', 'Generate initial admin token and exit') do
|
|
644
656
|
options[:init_admin] = true
|
|
645
657
|
end
|
|
@@ -715,6 +727,72 @@ if options[:version]
|
|
|
715
727
|
exit
|
|
716
728
|
end
|
|
717
729
|
|
|
730
|
+
# Handle --gen-cert (generate self-signed TLS certificate and exit)
|
|
731
|
+
if options[:gen_cert]
|
|
732
|
+
require 'kairos_mcp/tls_config'
|
|
733
|
+
require 'kairos_mcp/tls_cert_generator'
|
|
734
|
+
require 'kairos_mcp/skills_config'
|
|
735
|
+
|
|
736
|
+
http_config = KairosMcp::SkillsConfig.load['http'] || {}
|
|
737
|
+
tls = KairosMcp::TlsConfig.new(http_config, data_dir: KairosMcp.data_dir, force_enabled: true)
|
|
738
|
+
|
|
739
|
+
cert_exists = File.exist?(tls.cert_path) || File.exist?(tls.key_path)
|
|
740
|
+
if cert_exists
|
|
741
|
+
$stderr.puts "[gen-cert] A cert/key already exists:"
|
|
742
|
+
$stderr.puts " cert: #{tls.cert_path}" if File.exist?(tls.cert_path)
|
|
743
|
+
$stderr.puts " key: #{tls.key_path}" if File.exist?(tls.key_path)
|
|
744
|
+
if KairosMcp::TlsCertGenerator.overwrite_refused_noninteractive?(exists: cert_exists, tty: $stdin.tty?)
|
|
745
|
+
$stderr.puts "[gen-cert] Non-interactive mode: refusing to overwrite. Delete the files to regenerate."
|
|
746
|
+
exit 1
|
|
747
|
+
else
|
|
748
|
+
$stderr.puts ""
|
|
749
|
+
$stderr.puts "Overwrite? (y/N)"
|
|
750
|
+
answer = $stdin.gets&.strip
|
|
751
|
+
exit unless answer&.downcase == 'y'
|
|
752
|
+
end
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
# Build the SAN host list: loopback defaults + system hostname + config host
|
|
756
|
+
# + any --cert-host entries. Without this the cert only covers localhost and
|
|
757
|
+
# remote clients connecting by hostname/IP fail certificate verification.
|
|
758
|
+
require 'socket'
|
|
759
|
+
san_hosts = KairosMcp::TlsCertGenerator::DEFAULT_HOSTS.dup
|
|
760
|
+
begin
|
|
761
|
+
san_hosts << Socket.gethostname
|
|
762
|
+
rescue StandardError
|
|
763
|
+
# hostname is best-effort; loopback defaults still apply
|
|
764
|
+
end
|
|
765
|
+
cfg_host = http_config['host']
|
|
766
|
+
san_hosts << cfg_host unless cfg_host.nil? || ['0.0.0.0', '::', ''].include?(cfg_host)
|
|
767
|
+
san_hosts.concat(Array(options[:cert_hosts]))
|
|
768
|
+
|
|
769
|
+
result = KairosMcp::TlsCertGenerator.generate(
|
|
770
|
+
cert_path: tls.cert_path, key_path: tls.key_path, hosts: san_hosts
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
$stderr.puts ""
|
|
774
|
+
$stderr.puts "=" * 60
|
|
775
|
+
$stderr.puts " Self-signed TLS certificate generated"
|
|
776
|
+
$stderr.puts "=" * 60
|
|
777
|
+
$stderr.puts " cert: #{result[:cert_path]}"
|
|
778
|
+
$stderr.puts " key: #{result[:key_path]} (chmod 0600)"
|
|
779
|
+
$stderr.puts " expires: #{result[:not_after]}"
|
|
780
|
+
$stderr.puts " SAN: #{result[:san]}"
|
|
781
|
+
$stderr.puts ""
|
|
782
|
+
$stderr.puts " If you access this server by a hostname/IP not listed in SAN,"
|
|
783
|
+
$stderr.puts " regenerate with: kairos-chain --gen-cert --cert-host <name>"
|
|
784
|
+
$stderr.puts ""
|
|
785
|
+
$stderr.puts " Enable TLS by either:"
|
|
786
|
+
$stderr.puts " - starting with: kairos-chain --http --tls"
|
|
787
|
+
$stderr.puts " - or setting http.tls.enabled: true in skills/config.yml"
|
|
788
|
+
$stderr.puts ""
|
|
789
|
+
$stderr.puts " NOTE: self-signed certs are for single-operator use. Clients"
|
|
790
|
+
$stderr.puts " must trust this cert (or disable verification). For a public"
|
|
791
|
+
$stderr.puts " service use a CA-issued cert behind a reverse proxy."
|
|
792
|
+
$stderr.puts "=" * 60
|
|
793
|
+
exit
|
|
794
|
+
end
|
|
795
|
+
|
|
718
796
|
# Handle --init-admin
|
|
719
797
|
if options[:init_admin]
|
|
720
798
|
require 'kairos_mcp/auth/token_store'
|
|
@@ -734,6 +812,13 @@ if options[:init_admin]
|
|
|
734
812
|
store_path = File.join(KairosMcp.data_dir, store_path)
|
|
735
813
|
end
|
|
736
814
|
|
|
815
|
+
# Resolve the actual scheme from TlsConfig (config OR --tls), not from the
|
|
816
|
+
# --tls flag alone — otherwise config-enabled TLS prints an http:// URL.
|
|
817
|
+
require 'kairos_mcp/tls_config'
|
|
818
|
+
admin_scheme = KairosMcp::TlsConfig.new(
|
|
819
|
+
http_config, data_dir: KairosMcp.data_dir, force_enabled: options[:tls]
|
|
820
|
+
).scheme
|
|
821
|
+
|
|
737
822
|
store = KairosMcp::Auth::TokenStore.create(
|
|
738
823
|
backend: http_config['token_backend'],
|
|
739
824
|
store_path: store_path
|
|
@@ -790,9 +875,9 @@ if options[:init_admin]
|
|
|
790
875
|
$stderr.puts " {"
|
|
791
876
|
$stderr.puts " \"mcpServers\": {"
|
|
792
877
|
$stderr.puts " \"kairos\": {"
|
|
793
|
-
$stderr.puts " \"url\": \"
|
|
878
|
+
$stderr.puts " \"url\": \"#{admin_scheme}://localhost:#{options[:port] || 8080}/mcp\","
|
|
794
879
|
$stderr.puts " \"headers\": {"
|
|
795
|
-
$stderr.puts " \"Authorization\": \"Bearer #{result['raw_token']}\""
|
|
880
|
+
$stderr.puts " \"Authorization\": \"Bearer #{result[:raw_token] || result['raw_token']}\""
|
|
796
881
|
$stderr.puts " }"
|
|
797
882
|
$stderr.puts " }"
|
|
798
883
|
$stderr.puts " }"
|
|
@@ -817,7 +902,8 @@ if options[:http]
|
|
|
817
902
|
server = KairosMcp::HttpServer.new(
|
|
818
903
|
port: options[:port],
|
|
819
904
|
host: options[:host],
|
|
820
|
-
token_store_path: options[:token_store]
|
|
905
|
+
token_store_path: options[:token_store],
|
|
906
|
+
tls: options[:tls]
|
|
821
907
|
)
|
|
822
908
|
server.run
|
|
823
909
|
else
|
|
@@ -9,6 +9,7 @@ require_relative '../kairos_mcp'
|
|
|
9
9
|
require_relative 'auth/token_store'
|
|
10
10
|
require_relative 'auth/authenticator'
|
|
11
11
|
require_relative 'skills_config'
|
|
12
|
+
require_relative 'tls_config'
|
|
12
13
|
require_relative 'admin/router'
|
|
13
14
|
require_relative 'meeting_router'
|
|
14
15
|
|
|
@@ -39,14 +40,19 @@ module KairosMcp
|
|
|
39
40
|
'Cache-Control' => 'no-cache'
|
|
40
41
|
}.freeze
|
|
41
42
|
|
|
42
|
-
attr_reader :port, :host, :token_store, :authenticator, :admin_router,
|
|
43
|
+
attr_reader :port, :host, :token_store, :authenticator, :admin_router,
|
|
44
|
+
:meeting_router, :place_router, :tls
|
|
43
45
|
|
|
44
|
-
def initialize(port: nil, host: nil, token_store_path: nil)
|
|
46
|
+
def initialize(port: nil, host: nil, token_store_path: nil, tls: nil)
|
|
45
47
|
http_config = SkillsConfig.load['http'] || {}
|
|
46
48
|
|
|
47
49
|
@port = port || http_config['port'] || DEFAULT_PORT
|
|
48
50
|
@host = host || http_config['host'] || DEFAULT_HOST
|
|
49
51
|
|
|
52
|
+
# TLS is opt-in and delegated to Puma/OpenSSL (Prop 2: execution
|
|
53
|
+
# substrate). `tls: true` from the CLI (--tls) overrides the config.
|
|
54
|
+
@tls = TlsConfig.new(http_config, data_dir: KairosMcp.data_dir, force_enabled: tls)
|
|
55
|
+
|
|
50
56
|
# SkillSets must load BEFORE TokenStore.create so that plugins
|
|
51
57
|
# (e.g. Multiuser) can register alternative backends first.
|
|
52
58
|
eager_load_skillsets
|
|
@@ -79,6 +85,7 @@ module KairosMcp
|
|
|
79
85
|
def run
|
|
80
86
|
KairosMcp.http_server = self
|
|
81
87
|
check_dependencies!
|
|
88
|
+
check_tls!
|
|
82
89
|
check_tokens!
|
|
83
90
|
check_version_mismatch
|
|
84
91
|
auto_start_meeting_place
|
|
@@ -87,7 +94,9 @@ module KairosMcp
|
|
|
87
94
|
server = self
|
|
88
95
|
|
|
89
96
|
log "Starting KairosChain MCP Server v#{VERSION} (Streamable HTTP)"
|
|
90
|
-
log "Listening on #{@host}:#{@port}"
|
|
97
|
+
log "Listening on #{@tls.scheme}://#{@host}:#{@port}"
|
|
98
|
+
log "TLS: #{@tls.enabled? ? "enabled (cert: #{@tls.cert_path})" : 'disabled (plain HTTP)'}"
|
|
99
|
+
log_tls_expiry
|
|
91
100
|
log "MCP endpoint: POST /mcp"
|
|
92
101
|
log "Health check: GET /health"
|
|
93
102
|
log "Admin UI: GET /admin"
|
|
@@ -99,7 +108,7 @@ module KairosMcp
|
|
|
99
108
|
require 'puma/launcher'
|
|
100
109
|
|
|
101
110
|
puma_config = Puma::Configuration.new do |config|
|
|
102
|
-
config.bind
|
|
111
|
+
config.bind server.bind_uri
|
|
103
112
|
config.app app
|
|
104
113
|
config.workers 0
|
|
105
114
|
config.threads 1, 5
|
|
@@ -176,6 +185,7 @@ module KairosMcp
|
|
|
176
185
|
server: 'kairos-chain',
|
|
177
186
|
version: KairosMcp::VERSION,
|
|
178
187
|
transport: 'streamable-http',
|
|
188
|
+
tls: @tls.enabled?,
|
|
179
189
|
tokens_configured: !@token_store.empty?,
|
|
180
190
|
place_started: !@place_router.nil?
|
|
181
191
|
}
|
|
@@ -296,8 +306,63 @@ module KairosMcp
|
|
|
296
306
|
[status, JSON_HEADERS, [body_hash.to_json]]
|
|
297
307
|
end
|
|
298
308
|
|
|
309
|
+
# Puma bind URI for the configured host/port. Scheme (tcp:// vs ssl://)
|
|
310
|
+
# is decided by TlsConfig. Public for testability.
|
|
311
|
+
def bind_uri
|
|
312
|
+
@tls.bind_uri(@host, @port)
|
|
313
|
+
end
|
|
314
|
+
|
|
299
315
|
private
|
|
300
316
|
|
|
317
|
+
# Fail-closed TLS check: if TLS is enabled but cert/key are missing,
|
|
318
|
+
# abort with actionable guidance rather than starting plain HTTP.
|
|
319
|
+
def check_tls!
|
|
320
|
+
@tls.validate!
|
|
321
|
+
rescue TlsConfigError => e
|
|
322
|
+
$stderr.puts "[ERROR] #{e.message}"
|
|
323
|
+
exit 1
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Surface certificate expiry so a self-signed cert does not silently stop
|
|
327
|
+
# working after its validity window (no auto-renewal for --gen-cert).
|
|
328
|
+
EXPIRY_WARN_DAYS = 30
|
|
329
|
+
|
|
330
|
+
def log_tls_expiry
|
|
331
|
+
return unless @tls.enabled?
|
|
332
|
+
|
|
333
|
+
days = @tls.days_until_expiry
|
|
334
|
+
return if days.nil?
|
|
335
|
+
|
|
336
|
+
not_after = @tls.certificate_not_after
|
|
337
|
+
if days.negative?
|
|
338
|
+
$stderr.puts "[WARN] TLS certificate EXPIRED #{-days} day(s) ago (#{not_after}). " \
|
|
339
|
+
"Regenerate with 'kairos-chain --gen-cert' (delete the old cert first)."
|
|
340
|
+
else
|
|
341
|
+
log "TLS cert expires: #{not_after} (#{days} days)"
|
|
342
|
+
if days <= EXPIRY_WARN_DAYS
|
|
343
|
+
$stderr.puts "[WARN] TLS certificate expires in #{days} day(s). " \
|
|
344
|
+
"Regenerate with 'kairos-chain --gen-cert' (delete the old cert first)."
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
LOOPBACK_HOSTS = ['127.0.0.1', '::1', 'localhost'].freeze
|
|
350
|
+
|
|
351
|
+
# Environment opt-out for the fail-closed open-endpoint guard.
|
|
352
|
+
ALLOW_OPEN_ENDPOINT_ENV = 'KAIROS_ALLOW_OPEN_ENDPOINT'
|
|
353
|
+
|
|
354
|
+
def loopback_only?
|
|
355
|
+
LOOPBACK_HOSTS.include?(@host)
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Pure predicate (class method for testability): an empty token store means
|
|
359
|
+
# unauthenticated owner access. That is only acceptable on a loopback bind
|
|
360
|
+
# without TLS (local dev). Network-reachable (non-loopback) or TLS-enabled
|
|
361
|
+
# (remote intent) + empty store = an open owner endpoint.
|
|
362
|
+
def self.exposed_without_auth?(token_store_empty:, loopback:, tls_enabled:)
|
|
363
|
+
token_store_empty && (!loopback || tls_enabled)
|
|
364
|
+
end
|
|
365
|
+
|
|
301
366
|
# Register place extensions from enabled SkillSets that declare place_extensions.
|
|
302
367
|
# Uses KairosMcp.http_server pattern for late registration access.
|
|
303
368
|
def register_place_extensions(router)
|
|
@@ -367,13 +432,49 @@ module KairosMcp
|
|
|
367
432
|
end
|
|
368
433
|
|
|
369
434
|
def check_tokens!
|
|
370
|
-
|
|
435
|
+
return unless @token_store.empty?
|
|
436
|
+
|
|
437
|
+
# An empty token store means the MCP endpoint accepts unauthenticated
|
|
438
|
+
# owner-level requests. On loopback without TLS that is convenient
|
|
439
|
+
# local-dev. Exposed over the network (non-loopback, or TLS = remote
|
|
440
|
+
# intent) it is an OPEN OWNER ENDPOINT — encrypting an open door.
|
|
441
|
+
exposed = self.class.exposed_without_auth?(
|
|
442
|
+
token_store_empty: true, loopback: loopback_only?, tls_enabled: @tls.enabled?
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
unless exposed
|
|
371
446
|
$stderr.puts <<~MSG
|
|
372
447
|
[INFO] Local dev mode: no tokens configured.
|
|
373
448
|
MCP endpoint accepts unauthenticated requests as local owner.
|
|
374
449
|
For production, generate a token with: kairos-chain --init-admin
|
|
375
450
|
|
|
376
451
|
MSG
|
|
452
|
+
return
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# Fail-closed: refuse to start an open owner endpoint on a reachable
|
|
456
|
+
# bind. The operator can opt out explicitly for intentional cases.
|
|
457
|
+
if ENV[ALLOW_OPEN_ENDPOINT_ENV] == '1'
|
|
458
|
+
$stderr.puts <<~MSG
|
|
459
|
+
[SECURITY] No tokens configured and the endpoint is network-reachable
|
|
460
|
+
(host=#{@host}#{@tls.enabled? ? ', TLS enabled' : ''}). Starting anyway
|
|
461
|
+
because #{ALLOW_OPEN_ENDPOINT_ENV}=1. Unauthenticated requests are
|
|
462
|
+
accepted as owner — anyone who can reach this port has full owner access.
|
|
463
|
+
|
|
464
|
+
MSG
|
|
465
|
+
else
|
|
466
|
+
$stderr.puts <<~MSG
|
|
467
|
+
[ERROR] Refusing to start: no tokens configured but the endpoint is
|
|
468
|
+
network-reachable (host=#{@host}#{@tls.enabled? ? ', TLS enabled' : ''}).
|
|
469
|
+
Unauthenticated requests would be accepted as owner — TLS encrypts the
|
|
470
|
+
traffic but does NOT authenticate it, so this is an open owner endpoint.
|
|
471
|
+
|
|
472
|
+
Generate an admin token first: kairos-chain --init-admin
|
|
473
|
+
Or bind to loopback (host 127.0.0.1) for local-only use.
|
|
474
|
+
To start intentionally without auth, set #{ALLOW_OPEN_ENDPOINT_ENV}=1.
|
|
475
|
+
|
|
476
|
+
MSG
|
|
477
|
+
exit 1
|
|
377
478
|
end
|
|
378
479
|
end
|
|
379
480
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'openssl'
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
require 'ipaddr'
|
|
7
|
+
|
|
8
|
+
module KairosMcp
|
|
9
|
+
# TlsCertGenerator: produce a self-signed certificate + key for the
|
|
10
|
+
# single-operator remote-access case.
|
|
11
|
+
#
|
|
12
|
+
# Design intent (Prop 2): this uses Ruby's OpenSSL stdlib to *generate* key
|
|
13
|
+
# material — it does not implement any cryptography of its own. It exists so
|
|
14
|
+
# that "kairos-chain --gen-cert" gives a one-shot path to HTTPS for a single
|
|
15
|
+
# operator. For a public, multi-user service, use a CA-issued certificate
|
|
16
|
+
# (e.g. behind a reverse proxy) instead of a self-signed one.
|
|
17
|
+
module TlsCertGenerator
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# Hostnames/IPs always covered by the generated certificate's SAN so that
|
|
21
|
+
# loopback access verifies without --cert-host. Remote names are added by
|
|
22
|
+
# the caller (system hostname, config host, --cert-host entries).
|
|
23
|
+
DEFAULT_HOSTS = %w[localhost 127.0.0.1 ::1].freeze
|
|
24
|
+
|
|
25
|
+
# @param hosts [Array<String>] hostnames/IPs to place in the SAN. Each entry
|
|
26
|
+
# is auto-classified as IP: or DNS:. The common_name is always included.
|
|
27
|
+
# @return [Hash] { cert_path:, key_path:, not_after:, san: }
|
|
28
|
+
def generate(cert_path:, key_path:, common_name: 'kairos-chain',
|
|
29
|
+
hosts: DEFAULT_HOSTS, days: 825, key_size: 2048)
|
|
30
|
+
key = OpenSSL::PKey::RSA.new(key_size)
|
|
31
|
+
|
|
32
|
+
name = OpenSSL::X509::Name.parse("/CN=#{common_name}")
|
|
33
|
+
cert = OpenSSL::X509::Certificate.new
|
|
34
|
+
cert.version = 2
|
|
35
|
+
# Positive, non-zero serial. RFC 5280 requires a positive integer;
|
|
36
|
+
# SecureRandom.random_number(1 << 64) can return 0, so add 1.
|
|
37
|
+
cert.serial = OpenSSL::BN.new(SecureRandom.random_number(1 << 64) + 1)
|
|
38
|
+
cert.subject = name
|
|
39
|
+
cert.issuer = name
|
|
40
|
+
cert.public_key = key.public_key
|
|
41
|
+
|
|
42
|
+
now = Time.now
|
|
43
|
+
cert.not_before = now - 3600 # tolerate minor clock skew
|
|
44
|
+
cert.not_after = now + (days * 24 * 3600)
|
|
45
|
+
|
|
46
|
+
san = san_value(common_name, hosts)
|
|
47
|
+
|
|
48
|
+
ef = OpenSSL::X509::ExtensionFactory.new
|
|
49
|
+
ef.subject_certificate = cert
|
|
50
|
+
ef.issuer_certificate = cert
|
|
51
|
+
# Leaf SERVER certificate — explicitly NOT a CA, scoped to serverAuth so
|
|
52
|
+
# strict clients (macOS Secure Transport, Chromium) accept it.
|
|
53
|
+
cert.add_extension(ef.create_extension('basicConstraints', 'CA:FALSE', true))
|
|
54
|
+
cert.add_extension(ef.create_extension('keyUsage', 'digitalSignature,keyEncipherment', true))
|
|
55
|
+
cert.add_extension(ef.create_extension('extendedKeyUsage', 'serverAuth', false))
|
|
56
|
+
cert.add_extension(ef.create_extension('subjectAltName', san, false))
|
|
57
|
+
cert.sign(key, OpenSSL::Digest.new('SHA256'))
|
|
58
|
+
|
|
59
|
+
write_secure(cert_path, cert.to_pem, 0o644)
|
|
60
|
+
write_secure(key_path, key.to_pem, 0o600)
|
|
61
|
+
|
|
62
|
+
{ cert_path: cert_path, key_path: key_path, not_after: cert.not_after, san: san }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Build a subjectAltName value from a host list, auto-classifying each
|
|
66
|
+
# entry as IP: or DNS:. The common_name is always included as DNS.
|
|
67
|
+
def san_value(common_name, hosts)
|
|
68
|
+
entries = []
|
|
69
|
+
add_san(entries, common_name)
|
|
70
|
+
Array(hosts).each { |h| add_san(entries, h) }
|
|
71
|
+
entries.uniq.join(',')
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def add_san(entries, host)
|
|
75
|
+
return if host.nil? || host.to_s.strip.empty?
|
|
76
|
+
|
|
77
|
+
h = host.to_s.strip
|
|
78
|
+
# Reject injection: a raw entry must be a single hostname/IP, not a
|
|
79
|
+
# comma-separated list or a pre-formatted "DNS:/IP:" token — otherwise
|
|
80
|
+
# --cert-host "a,DNS:evil" would silently mint extra SAN names.
|
|
81
|
+
if h.include?(',') || h =~ /\A(DNS|IP):/i
|
|
82
|
+
raise ArgumentError, "invalid SAN host #{h.inspect}: must be a single hostname or IP"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
begin
|
|
86
|
+
IPAddr.new(h)
|
|
87
|
+
entries << "IP:#{h}"
|
|
88
|
+
rescue IPAddr::InvalidAddressError, ArgumentError
|
|
89
|
+
entries << "DNS:#{h}"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Deterministic half of the --gen-cert overwrite guard: in non-interactive
|
|
94
|
+
# mode, refuse to clobber an existing cert/key (which could break a live
|
|
95
|
+
# deployment). Interactive mode prompts the operator instead. Extracted so
|
|
96
|
+
# the refusal branch is unit-testable.
|
|
97
|
+
def overwrite_refused_noninteractive?(exists:, tty:)
|
|
98
|
+
exists && !tty
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def write_secure(path, content, mode)
|
|
102
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
103
|
+
# Open with target mode (new files) AND chmod the descriptor BEFORE
|
|
104
|
+
# writing, so on OVERWRITE of a pre-existing world-readable file the key
|
|
105
|
+
# bytes are never written while the old permissive mode is still in
|
|
106
|
+
# effect. Closes the umask/overwrite window on both create and overwrite.
|
|
107
|
+
File.open(path, File::WRONLY | File::CREAT | File::TRUNC, mode) do |f|
|
|
108
|
+
f.chmod(mode)
|
|
109
|
+
f.write(content)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
require 'openssl'
|
|
5
|
+
|
|
6
|
+
module KairosMcp
|
|
7
|
+
# TlsConfig: resolves optional TLS settings for the HTTP transport.
|
|
8
|
+
#
|
|
9
|
+
# Design intent (Prop 2, partial autopoiesis): transport encryption is an
|
|
10
|
+
# execution-substrate concern, not a self-referential core capability.
|
|
11
|
+
# KairosChain does NOT implement crypto here. It delegates to Puma/OpenSSL
|
|
12
|
+
# and only decides the bind scheme (tcp:// vs ssl://) from config. The only
|
|
13
|
+
# logic in this class is path resolution and fail-closed validation.
|
|
14
|
+
#
|
|
15
|
+
# Config shape (under the 'http' key of skills/config.yml):
|
|
16
|
+
# http:
|
|
17
|
+
# tls:
|
|
18
|
+
# enabled: false
|
|
19
|
+
# cert: "storage/tls/cert.pem"
|
|
20
|
+
# key: "storage/tls/key.pem"
|
|
21
|
+
#
|
|
22
|
+
class TlsConfig
|
|
23
|
+
DEFAULT_CERT = 'storage/tls/cert.pem'
|
|
24
|
+
DEFAULT_KEY = 'storage/tls/key.pem'
|
|
25
|
+
|
|
26
|
+
attr_reader :cert_path, :key_path
|
|
27
|
+
|
|
28
|
+
# @param http_config [Hash] the 'http' section of the loaded config
|
|
29
|
+
# @param data_dir [String] base dir for resolving relative cert/key paths
|
|
30
|
+
# @param force_enabled [Boolean, nil] CLI override; nil = use config value
|
|
31
|
+
def initialize(http_config, data_dir:, force_enabled: nil)
|
|
32
|
+
tls = (http_config || {})['tls'] || {}
|
|
33
|
+
@data_dir = data_dir
|
|
34
|
+
@enabled = force_enabled.nil? ? (tls['enabled'] == true) : force_enabled
|
|
35
|
+
# An empty-string path in config falls back to the default rather than
|
|
36
|
+
# resolving to nil (which would crash the --gen-cert path).
|
|
37
|
+
@cert_path = resolve(present_or(tls['cert'], DEFAULT_CERT))
|
|
38
|
+
@key_path = resolve(present_or(tls['key'], DEFAULT_KEY))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def enabled?
|
|
42
|
+
@enabled
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Fail-closed validation. When TLS is enabled, the cert and key must exist,
|
|
46
|
+
# be readable, and parse as valid material — otherwise abort rather than
|
|
47
|
+
# start plain HTTP or crash later inside Puma with an opaque error.
|
|
48
|
+
def validate!
|
|
49
|
+
return unless @enabled
|
|
50
|
+
|
|
51
|
+
problems = []
|
|
52
|
+
problems << "certificate is missing (#{@cert_path})" unless @cert_path && File.exist?(@cert_path)
|
|
53
|
+
problems << "private key is missing (#{@key_path})" unless @key_path && File.exist?(@key_path)
|
|
54
|
+
|
|
55
|
+
if problems.empty?
|
|
56
|
+
problems << "certificate is not readable (#{@cert_path})" unless File.readable?(@cert_path)
|
|
57
|
+
problems << "private key is not readable (#{@key_path})" unless File.readable?(@key_path)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
if problems.empty?
|
|
61
|
+
cert = safe_parse { OpenSSL::X509::Certificate.new(File.read(@cert_path)) }
|
|
62
|
+
key = safe_parse { OpenSSL::PKey.read(File.read(@key_path)) }
|
|
63
|
+
problems << "certificate is not valid PEM (#{@cert_path})" if cert.nil?
|
|
64
|
+
problems << "private key is not valid (#{@key_path})" if key.nil?
|
|
65
|
+
# A parseable cert + parseable key that do not form a pair would pass
|
|
66
|
+
# independent checks but fail opaquely inside Puma's SSL setup.
|
|
67
|
+
if cert && key && !cert.check_private_key(key)
|
|
68
|
+
problems << "certificate and private key do not match (#{@cert_path} / #{@key_path})"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
return if problems.empty?
|
|
73
|
+
|
|
74
|
+
raise TlsConfigError, <<~MSG.strip
|
|
75
|
+
TLS is enabled but its material is unusable:
|
|
76
|
+
- #{problems.join("\n - ")}
|
|
77
|
+
|
|
78
|
+
Generate a self-signed certificate for single-operator use:
|
|
79
|
+
kairos-chain --gen-cert
|
|
80
|
+
|
|
81
|
+
Or point http.tls.cert / http.tls.key at a valid certificate.
|
|
82
|
+
MSG
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Build a Puma bind URI. Encryption params are passed to Puma, which
|
|
86
|
+
# performs the TLS handshake via OpenSSL.
|
|
87
|
+
def bind_uri(host, port)
|
|
88
|
+
h = bracket_ipv6(host)
|
|
89
|
+
return "tcp://#{h}:#{port}" unless @enabled
|
|
90
|
+
|
|
91
|
+
query = URI.encode_www_form('key' => @key_path, 'cert' => @cert_path)
|
|
92
|
+
"ssl://#{h}:#{port}?#{query}"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def scheme
|
|
96
|
+
@enabled ? 'https' : 'http'
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Expiry (not_after) of the configured certificate, or nil if it cannot be
|
|
100
|
+
# read/parsed. Used to surface silent-expiry breakage at startup.
|
|
101
|
+
def certificate_not_after
|
|
102
|
+
return nil unless @cert_path && File.exist?(@cert_path)
|
|
103
|
+
|
|
104
|
+
OpenSSL::X509::Certificate.new(File.read(@cert_path)).not_after
|
|
105
|
+
rescue StandardError
|
|
106
|
+
nil
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Whole days until the certificate expires (negative if already expired),
|
|
110
|
+
# or nil if the cert cannot be read. now is injectable for testing.
|
|
111
|
+
def days_until_expiry(now = Time.now)
|
|
112
|
+
not_after = certificate_not_after
|
|
113
|
+
return nil unless not_after
|
|
114
|
+
|
|
115
|
+
((not_after - now) / 86_400).floor
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
# An IPv6 literal host must be bracketed in a URI authority
|
|
121
|
+
# (ssl://[::1]:8443), otherwise Puma's binder misparses the colons.
|
|
122
|
+
def bracket_ipv6(host)
|
|
123
|
+
s = host.to_s
|
|
124
|
+
return s if s.empty? || s.start_with?('[') || !s.include?(':')
|
|
125
|
+
|
|
126
|
+
"[#{s}]"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def present_or(value, default)
|
|
130
|
+
s = value.to_s.strip
|
|
131
|
+
s.empty? ? default : s
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def resolve(path)
|
|
135
|
+
return nil if path.nil? || path.to_s.empty?
|
|
136
|
+
|
|
137
|
+
File.absolute_path?(path) ? path : File.join(@data_dir, path)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def safe_parse
|
|
141
|
+
yield
|
|
142
|
+
rescue StandardError
|
|
143
|
+
nil
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Raised when TLS is enabled but its material cannot be located or used.
|
|
148
|
+
class TlsConfigError < StandardError; end
|
|
149
|
+
end
|
data/lib/kairos_mcp/version.rb
CHANGED
|
@@ -654,7 +654,10 @@ Options:
|
|
|
654
654
|
--data-dir DIR Data directory path (default: .kairos/ in current dir)
|
|
655
655
|
--http Start in Streamable HTTP mode (default: stdio)
|
|
656
656
|
--port PORT HTTP port (default: 8080)
|
|
657
|
-
--host HOST HTTP bind host (default:
|
|
657
|
+
--host HOST HTTP bind host (default: config http.host, else 127.0.0.1)
|
|
658
|
+
--tls Enable built-in TLS/HTTPS (requires a cert — see --gen-cert)
|
|
659
|
+
--gen-cert Generate a self-signed TLS cert/key and exit
|
|
660
|
+
--cert-host HOST Add a hostname/IP to the generated cert SAN (repeatable)
|
|
658
661
|
--init-admin Generate initial admin token and exit
|
|
659
662
|
--token-store PATH Path to token store file
|
|
660
663
|
-v, --version Show version
|
|
@@ -662,11 +665,41 @@ Options:
|
|
|
662
665
|
|
|
663
666
|
Environment Variables:
|
|
664
667
|
KAIROS_DATA_DIR Override data directory path
|
|
668
|
+
KAIROS_ALLOW_OPEN_ENDPOINT Set to 1 to start without a token on a network-reachable bind (unsafe)
|
|
665
669
|
```
|
|
666
670
|
|
|
667
|
-
####
|
|
671
|
+
#### HTTPS / TLS
|
|
668
672
|
|
|
669
|
-
|
|
673
|
+
There are two ways to serve the MCP endpoint over TLS. Encryption is always performed by
|
|
674
|
+
Puma/OpenSSL — KairosChain only selects the transport (plain HTTP vs TLS).
|
|
675
|
+
|
|
676
|
+
**Option A: Built-in TLS (single operator / remote access)**
|
|
677
|
+
|
|
678
|
+
KairosChain can terminate TLS itself, so a self-hosted instance can be reached securely
|
|
679
|
+
without a separate proxy.
|
|
680
|
+
|
|
681
|
+
```bash
|
|
682
|
+
# 1. Generate a self-signed cert/key once (written to <data-dir>/storage/tls/).
|
|
683
|
+
# Include every hostname/IP you connect by, so certificate verification passes:
|
|
684
|
+
kairos-chain --gen-cert --cert-host myhost.example.com
|
|
685
|
+
|
|
686
|
+
# 2. Start with TLS (or set http.tls.enabled: true in skills/config.yml):
|
|
687
|
+
kairos-chain --http --tls
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
Notes:
|
|
691
|
+
- The certificate is self-signed, so the client (Cursor / Claude Code) must trust it
|
|
692
|
+
(or disable verification). For a CA-issued certificate, use Option B.
|
|
693
|
+
- Certificates expire (default ~2 years). The server logs the expiry at startup and
|
|
694
|
+
warns when it is near; regenerate by deleting the old cert and running `--gen-cert` again.
|
|
695
|
+
- If no admin token is configured, KairosChain refuses to start on a network-reachable
|
|
696
|
+
bind (that would be an open owner endpoint). Run `--init-admin` first, or set
|
|
697
|
+
`KAIROS_ALLOW_OPEN_ENDPOINT=1` to start without authentication intentionally.
|
|
698
|
+
|
|
699
|
+
**Option B: Reverse proxy (public / multi-user)**
|
|
700
|
+
|
|
701
|
+
For a public or multi-user deployment, place a reverse proxy in front of Puma; the proxy
|
|
702
|
+
terminates TLS with a CA-issued certificate.
|
|
670
703
|
|
|
671
704
|
```
|
|
672
705
|
Client (Cursor) ──HTTPS──▶ Reverse Proxy ──HTTP──▶ Puma (:8080)
|
|
@@ -674,7 +707,7 @@ Client (Cursor) ──HTTPS──▶ Reverse Proxy ──HTTP──▶ Puma (:80
|
|
|
674
707
|
TLS termination
|
|
675
708
|
```
|
|
676
709
|
|
|
677
|
-
**
|
|
710
|
+
**Caddy (Recommended — Simplest)**
|
|
678
711
|
|
|
679
712
|
Caddy provides automatic HTTPS with Let's Encrypt certificates (zero configuration for TLS).
|
|
680
713
|
|
|
@@ -638,7 +638,10 @@ Usage: kairos-chain [command] [options]
|
|
|
638
638
|
--data-dir DIR データディレクトリのパス(デフォルト: カレントディレクトリの .kairos/)
|
|
639
639
|
--http Streamable HTTPモードで起動(デフォルト: stdio)
|
|
640
640
|
--port PORT HTTPポート(デフォルト: 8080)
|
|
641
|
-
--host HOST HTTPバインドホスト(デフォルト:
|
|
641
|
+
--host HOST HTTPバインドホスト(デフォルト: config の http.host、無ければ 127.0.0.1)
|
|
642
|
+
--tls 内蔵TLS/HTTPSを有効化(証明書が必要 — --gen-cert を参照)
|
|
643
|
+
--gen-cert 自己署名のTLS証明書/鍵を生成して終了
|
|
644
|
+
--cert-host HOST 生成する証明書のSANにホスト名/IPを追加(繰り返し指定可)
|
|
642
645
|
--init-admin 初期管理者トークンを生成して終了
|
|
643
646
|
--token-store PATH トークンストアファイルのパス
|
|
644
647
|
-v, --version バージョン表示
|
|
@@ -646,11 +649,34 @@ Usage: kairos-chain [command] [options]
|
|
|
646
649
|
|
|
647
650
|
環境変数:
|
|
648
651
|
KAIROS_DATA_DIR データディレクトリのパスを上書き
|
|
652
|
+
KAIROS_ALLOW_OPEN_ENDPOINT 1 にすると、ネットワーク到達可能なバインドでもトークン無しで起動(危険)
|
|
649
653
|
```
|
|
650
654
|
|
|
651
|
-
####
|
|
655
|
+
#### HTTPS / TLS
|
|
652
656
|
|
|
653
|
-
|
|
657
|
+
MCPエンドポイントをTLSで提供する方法は2つあります。暗号化は常に Puma/OpenSSL が行い、KairosChain は通信路(平文HTTP か TLS か)を選ぶだけです。
|
|
658
|
+
|
|
659
|
+
**オプションA: 内蔵TLS(単独運用者 / 遠隔アクセス)**
|
|
660
|
+
|
|
661
|
+
KairosChain は自分でTLSを終端できるので、別途プロキシを立てなくても自ホストのインスタンスに安全に接続できます。
|
|
662
|
+
|
|
663
|
+
```bash
|
|
664
|
+
# 1. 自己署名の証明書/鍵を一度だけ生成(<data-dir>/storage/tls/ に出力)。
|
|
665
|
+
# 接続に使うホスト名/IPを含めておくと、証明書の検証が通ります:
|
|
666
|
+
kairos-chain --gen-cert --cert-host myhost.example.com
|
|
667
|
+
|
|
668
|
+
# 2. TLSで起動(または skills/config.yml で http.tls.enabled: true を設定):
|
|
669
|
+
kairos-chain --http --tls
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
補足:
|
|
673
|
+
- 証明書は自己署名なので、クライアント(Cursor / Claude Code)側で信頼する(または検証を無効化する)必要があります。認証局発行の証明書が必要な場合はオプションBを使ってください。
|
|
674
|
+
- 証明書には有効期限(既定で約2年)があります。サーバは起動時に期限を記録し、期限が近いと警告します。更新は、古い証明書を削除して `--gen-cert` を再実行します。
|
|
675
|
+
- 管理者トークンが未設定の場合、ネットワーク到達可能なバインドでは起動を拒否します(無認証のowner露出になるため)。先に `--init-admin` を実行するか、意図的に無認証で起動する場合は `KAIROS_ALLOW_OPEN_ENDPOINT=1` を設定します。
|
|
676
|
+
|
|
677
|
+
**オプションB: リバースプロキシ(公開 / マルチユーザー)**
|
|
678
|
+
|
|
679
|
+
公開・マルチユーザー運用では、Pumaの前段にリバースプロキシを配置します。プロキシが認証局発行の証明書でTLSを終端します。
|
|
654
680
|
|
|
655
681
|
```
|
|
656
682
|
クライアント (Cursor) ──HTTPS──▶ リバースプロキシ ──HTTP──▶ Puma (:8080)
|
|
@@ -658,7 +684,7 @@ Usage: kairos-chain [command] [options]
|
|
|
658
684
|
TLS終端
|
|
659
685
|
```
|
|
660
686
|
|
|
661
|
-
|
|
687
|
+
**Caddy(推奨 — 最もシンプル)**
|
|
662
688
|
|
|
663
689
|
CaddyはLet's Encrypt証明書による自動HTTPS(TLSの設定不要)を提供します。
|
|
664
690
|
|
data/templates/skills/config.yml
CHANGED
|
@@ -190,6 +190,21 @@ http:
|
|
|
190
190
|
token_store: "storage/tokens.json" # Token storage path (uses SQLite if backend is sqlite)
|
|
191
191
|
default_token_expiry_days: 90 # Default token expiry (days)
|
|
192
192
|
|
|
193
|
+
# -----------------------------------------------------------------------
|
|
194
|
+
# TLS (HTTPS) — optional transport encryption for remote access
|
|
195
|
+
# -----------------------------------------------------------------------
|
|
196
|
+
# Encryption is delegated to Puma/OpenSSL; KairosChain only selects the
|
|
197
|
+
# bind scheme (Prop 2: transport is execution substrate, not core logic).
|
|
198
|
+
# For a single operator, generate a self-signed cert once, then enable:
|
|
199
|
+
# kairos-chain --gen-cert
|
|
200
|
+
# kairos-chain --http --tls # or set enabled: true below
|
|
201
|
+
# For a public service, prefer a reverse proxy (e.g. Caddy) with a
|
|
202
|
+
# CA-issued certificate instead of a self-signed cert.
|
|
203
|
+
tls:
|
|
204
|
+
enabled: false # opt-in; false = plain HTTP (default)
|
|
205
|
+
cert: "storage/tls/cert.pem" # certificate path (relative to data dir)
|
|
206
|
+
key: "storage/tls/key.pem" # private key path (chmod 0600)
|
|
207
|
+
|
|
193
208
|
# -----------------------------------------------------------------------
|
|
194
209
|
# Phase 2 (Future): Role-Based Authorization
|
|
195
210
|
# -----------------------------------------------------------------------
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kairos-chain
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.40.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Masaomi Hatakeyama
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: minitest
|
|
@@ -172,6 +172,8 @@ files:
|
|
|
172
172
|
- lib/kairos_mcp/storage/file_backend.rb
|
|
173
173
|
- lib/kairos_mcp/storage/importer.rb
|
|
174
174
|
- lib/kairos_mcp/storage/sqlite_backend.rb
|
|
175
|
+
- lib/kairos_mcp/tls_cert_generator.rb
|
|
176
|
+
- lib/kairos_mcp/tls_config.rb
|
|
175
177
|
- lib/kairos_mcp/tool_registry.rb
|
|
176
178
|
- lib/kairos_mcp/tools/base_tool.rb
|
|
177
179
|
- lib/kairos_mcp/tools/capability_status.rb
|