kairos-chain 3.35.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 +160 -0
- data/bin/kairos-chain +170 -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/design_to_implementation_workflow/design_to_implementation_workflow.md +5 -0
- data/templates/knowledge/kairoschain_setup/kairoschain_setup.md +37 -4
- data/templates/knowledge/kairoschain_setup_jp/kairoschain_setup_jp.md +30 -4
- data/templates/knowledge/multi_llm_review_workflow/multi_llm_review_workflow.md +181 -2
- data/templates/skills/config.yml +15 -0
- data/templates/skillsets/synoptis/config/synoptis.yml +10 -0
- data/templates/skillsets/synoptis/lib/synoptis/constitutive/attestation_chain.rb +53 -3
- data/templates/skillsets/synoptis/lib/synoptis/constitutive/content_attestation_entry.rb +17 -4
- data/templates/skillsets/synoptis/lib/synoptis/constitutive/proposal_criterion.rb +11 -3
- data/templates/skillsets/synoptis/lib/synoptis/constitutive/revocation_withdrawal_entry.rb +60 -0
- data/templates/skillsets/synoptis/lib/synoptis/constitutive/subject_ref.rb +43 -6
- data/templates/skillsets/synoptis/lib/synoptis/tool_helpers.rb +14 -1
- data/templates/skillsets/synoptis/lib/synoptis.rb +1 -0
- data/templates/skillsets/synoptis/skillset.json +3 -1
- data/templates/skillsets/synoptis/test/test_l2_constitutive_slice1.rb +4 -1
- data/templates/skillsets/synoptis/test/test_l2_constitutive_slice2.rb +121 -0
- data/templates/skillsets/synoptis/test/test_l2_constitutive_slice3.rb +117 -0
- data/templates/skillsets/synoptis/tools/l2_attestation_commit.rb +68 -20
- data/templates/skillsets/synoptis/tools/l2_attestation_revoke.rb +104 -0
- data/templates/skillsets/synoptis/tools/l2_attestation_scan.rb +8 -4
- data/templates/skillsets/synoptis/tools/l2_attestation_view.rb +64 -0
- metadata +9 -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,166 @@ 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
|
+
|
|
38
|
+
## [3.39.0] - 2026-07-05
|
|
39
|
+
|
|
40
|
+
### Added — L1 `multi_llm_review_workflow` v3.5 → v3.6: Step 0.25 Unknowns Pass
|
|
41
|
+
|
|
42
|
+
Adds a pre-draft unknown-discovery discipline to the multi-LLM review workflow,
|
|
43
|
+
adapted from Thariq Shihipar's "A Field Guide to Fable: Finding Your Unknowns"
|
|
44
|
+
(2026-07-03), techniques ① blindspot pass and ③ one-question-at-a-time interview.
|
|
45
|
+
Bundled L1 knowledge only — no code paths changed.
|
|
46
|
+
|
|
47
|
+
- **Step 0.25 (pre-draft, qualifying reviews only)**: before drafting a design or
|
|
48
|
+
knowledge/documentation artifact routed to full multi-LLM review, the orchestrator
|
|
49
|
+
enumerates its blind-spot questions (without self-answering) and interviews the human
|
|
50
|
+
one question at a time, then triages every surfaced unknown to Resolved / Declared /
|
|
51
|
+
Draft-deferred. Moves unknown discovery from the expensive channel (review rounds) to
|
|
52
|
+
the cheap channel (pre-draft dialogue).
|
|
53
|
+
- **Five invariants INV-U1–U5**: front-load gate (no dispatch before the pass);
|
|
54
|
+
human gate (no orchestrator self-answering); constitutive recording (unconditional L2
|
|
55
|
+
record, incl. zero-result); bounded demotion of declared unknowns (provenance bound +
|
|
56
|
+
inverted default, closing a laundering channel); human-only classification authority.
|
|
57
|
+
- **Unattended (autonomous loop) clause**: fail-closed — every surfaced unknown is
|
|
58
|
+
judgment-shaped by default and stops the run; only a human-authored mandate may
|
|
59
|
+
pre-classify categories as non-judgment-shaped, and unattended declarations gain no
|
|
60
|
+
reviewer-demotion power until human-ratified. Mandate expressiveness, ratification
|
|
61
|
+
protocol, and the pending-question queue are deferred to the Autonomous Growth Loop
|
|
62
|
+
guard track.
|
|
63
|
+
- Three-location L1 sync (instance `.kairos/knowledge/`, gem `knowledge/`, gem
|
|
64
|
+
`templates/knowledge/`); change recorded to blockchain (hash a74a11b7…). One-line
|
|
65
|
+
cross-reference added to `design_to_implementation_workflow`.
|
|
66
|
+
- Designed via its own 3-round self-referential multi-LLM review (R1 REVISE 2/6 →
|
|
67
|
+
R2 REVISE 2/6 → R3 4/6 APPROVE; design record
|
|
68
|
+
`docs/drafts/multi_llm_review_unknowns_pass_v0.3.1_FROZEN.md`).
|
|
69
|
+
|
|
70
|
+
## [3.38.0] - 2026-07-05
|
|
71
|
+
|
|
72
|
+
### Added — Synoptis L2 attestation: ACT-5 trigger sources + scan CLI (Slice 3b/3c)
|
|
73
|
+
|
|
74
|
+
Makes the ACT-5 trigger point distinguishable and invocable from a session-end
|
|
75
|
+
Stop hook, so firing + trigger-record logging can be guaranteed rather than relying
|
|
76
|
+
on the orchestrator remembering to run the tool.
|
|
77
|
+
|
|
78
|
+
- **Trigger source (3b)**: `l2_attestation_scan` gains a `trigger_source` argument
|
|
79
|
+
(`manual` / `orchestrator_session_end` / `session_end_hook`), recorded in the trigger
|
|
80
|
+
record (`AttestationChain#append_trigger(source:)`). ACT-5's "at least one defined
|
|
81
|
+
trigger point" is now distinguishable in the operational log, and the tool documents
|
|
82
|
+
its two auto-firing paths (orchestrator-at-session-end for delivery+approval; Stop hook
|
|
83
|
+
for a mechanical liveness guarantee).
|
|
84
|
+
- **Scan CLI (3c)**: new `kairos-chain attestation scan [--session SID] [--source SRC]
|
|
85
|
+
[--data-dir DIR]`. It loads the constitutive criterion, proposes a session's
|
|
86
|
+
judgment-bearing contexts, and appends a trigger record — propose-only, never attests
|
|
87
|
+
(ACT-1 keeps attestation human-approved through the MCP tool). This is the shell
|
|
88
|
+
entry point a session-end Stop hook calls. Verified end-to-end.
|
|
89
|
+
|
|
90
|
+
The two firing mechanisms are complementary: the orchestrator convention delivers
|
|
91
|
+
proposals to a present human for approval; the Stop hook guarantees the trigger fires
|
|
92
|
+
and is logged even when the orchestrator does not. The settings.json Stop-hook wiring is
|
|
93
|
+
a per-project harness step (not shipped in the gem). 17 Slice-3 unit tests green (incl.
|
|
94
|
+
trigger-source); Slices 1 (35) and 2 (25) unchanged.
|
|
95
|
+
|
|
96
|
+
## [3.37.0] - 2026-07-05
|
|
97
|
+
|
|
98
|
+
### Added — Synoptis L2 attestation: semantic criterion + snapshot (Slice 3a)
|
|
99
|
+
|
|
100
|
+
Matures the ACT-2 selection criterion and adds the optional content snapshot
|
|
101
|
+
(§Kinds), without embedding an LLM call inside the tool (selective-survival: the
|
|
102
|
+
in-Ruby LLM scoring is deferred until the frontmatter criterion is observed to be
|
|
103
|
+
insufficient).
|
|
104
|
+
|
|
105
|
+
- **Semantic layer (ACT-2)**: `l2_attestation_scan` now attaches a bounded content
|
|
106
|
+
`preview` to each proposal and surfaces a `rubric` (config `semantic_rubric`). The
|
|
107
|
+
orchestrator applies the rubric to the previews to decide which frontmatter-filtered
|
|
108
|
+
candidates to propose to the human — the LLM-semantic judgment lives in the
|
|
109
|
+
orchestrator, not in the tool. The criterion stays inspectable and revisable by
|
|
110
|
+
editing config (`judgment_types`, `semantic_rubric`, `preview_chars`).
|
|
111
|
+
- **Snapshot (§Kinds optional)**: `l2_attestation_commit(embed_snapshot: true)` embeds
|
|
112
|
+
the subject's current content in the entry, so audits needing the original wording
|
|
113
|
+
have it (LED-2a preserves it). The snapshot's SHA256 equals the entry digest, so it is
|
|
114
|
+
self-verifying. Bounded by `max_snapshot_bytes` (default 256KB); oversize content is
|
|
115
|
+
refused (`snapshot_too_large`) rather than truncated.
|
|
116
|
+
- `SubjectRef` gains `content_text` / `content_preview`.
|
|
117
|
+
|
|
118
|
+
Deferred (until observed need): in-Ruby `llm_client` scoring of candidates; session-end
|
|
119
|
+
auto-firing of the trigger (ACT-5 liveness is currently satisfied by explicit/orchestrator
|
|
120
|
+
scan). 12 Slice-3 unit tests + 4 tool integration tests green; Slices 1 (35) and 2 (25)
|
|
121
|
+
unchanged.
|
|
122
|
+
|
|
123
|
+
## [3.36.0] - 2026-07-05
|
|
124
|
+
|
|
125
|
+
### Added — Synoptis L2 attestation: supersession + revocation (Slice 2)
|
|
126
|
+
|
|
127
|
+
Records the rewrite/withdrawal lifecycle of a constitutive attestation by appending,
|
|
128
|
+
never editing (the bank-ledger correction model). Completes §Kinds' two attestation-
|
|
129
|
+
ledger entry kinds and satisfies LED-2b (lineage) and ACT-3 (approval binding).
|
|
130
|
+
|
|
131
|
+
- **Supersession**: a re-attestation of an already-attested subject is now a
|
|
132
|
+
supersession — `l2_attestation_commit` auto-derives the current head and commits its
|
|
133
|
+
entry_id as `target_ref` (ContentAttestationEntry gains `target_ref`). The first
|
|
134
|
+
attestation about a subject carries none.
|
|
135
|
+
- **Revocation-withdrawal**: new `RevocationWithdrawalEntry` kind (commits subject +
|
|
136
|
+
target_ref + moment, no digest/content) and a new `l2_attestation_revoke` tool. It
|
|
137
|
+
appends a withdrawal of the subject's current head; like any ledger entry it requires
|
|
138
|
+
human approval (ACT-1) and is itself append-only, so "claimed then withdrawn" survives.
|
|
139
|
+
- **ACT-3 binding**: the commit/revoke proposal returns the digest and the target it
|
|
140
|
+
would act on; the approving call echoes them as `expected_digest` /
|
|
141
|
+
`expected_target_ref`. If the live content changed (digest_mismatch) or the head moved
|
|
142
|
+
(target_moved) between proposal and approval, the append is refused, not silent.
|
|
143
|
+
- **Fold**: `AttestationChain#current_head` / `current_state` fold a subject's entries
|
|
144
|
+
(first → supersessions → withdrawals) into a status (none/attested/withdrawn); a new
|
|
145
|
+
read-only `l2_attestation_view` tool surfaces it. Fold ordering rule fixed per §11: a
|
|
146
|
+
revocation clears the head only when it targets the current head.
|
|
147
|
+
|
|
148
|
+
Synoptis's trust engine (duplicate hard-reject, TTL, revocation store) is untouched —
|
|
149
|
+
the constitutive ledger is a separate chain, so re-attest-as-supersession is native and
|
|
150
|
+
no reconciliation was needed. 25 Slice-2 unit tests + 14 tool integration tests green;
|
|
151
|
+
Slice 1 (35) unchanged.
|
|
152
|
+
|
|
153
|
+
## [3.35.1] - 2026-07-05
|
|
154
|
+
|
|
155
|
+
### Fixed — Synoptis L2 attestation: frontmatter parsing dropped all proposals
|
|
156
|
+
|
|
157
|
+
`l2_attestation_scan` surfaced zero proposals against real L2 contexts because
|
|
158
|
+
`SubjectRef.extract_frontmatter` used `YAML.safe_load` without permitting `Date`:
|
|
159
|
+
a `date:` field (present in every real context's frontmatter) raised
|
|
160
|
+
`Psych::DisallowedClass`, which was rescued to `nil`, so the `type` was never read
|
|
161
|
+
and no context matched the criterion. Caught by live end-to-end verification, not
|
|
162
|
+
by unit tests (whose fixtures lacked a `date:` field). Fix permits `[Date, Time]`
|
|
163
|
+
and adds a regex fallback for the single `type` scalar so the criterion is robust
|
|
164
|
+
against any frontmatter YAML that `safe_load` rejects. Unit fixtures now carry a
|
|
165
|
+
`date:` field to guard the regression.
|
|
166
|
+
|
|
7
167
|
## [3.35.0] - 2026-07-05
|
|
8
168
|
|
|
9
169
|
### Added — Synoptis: L2 constitutive attestation (Slice 1)
|
data/bin/kairos-chain
CHANGED
|
@@ -343,6 +343,86 @@ when 'upgrade'
|
|
|
343
343
|
end
|
|
344
344
|
exit
|
|
345
345
|
|
|
346
|
+
when 'attestation'
|
|
347
|
+
# Constitutive L2 attestation CLI (Slice 3c). Currently one action: `scan`, the
|
|
348
|
+
# ACT-5 trigger point invokable from a session-end Stop hook so that firing +
|
|
349
|
+
# trigger-record logging is guaranteed even when the orchestrator does not run the
|
|
350
|
+
# MCP tool. It proposes only and logs a trigger record; it never attests (ACT-1
|
|
351
|
+
# keeps attestation human-approved through the MCP tool).
|
|
352
|
+
ARGV.shift # Remove 'attestation'
|
|
353
|
+
|
|
354
|
+
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
|
355
|
+
require 'kairos_mcp'
|
|
356
|
+
|
|
357
|
+
if (idx = ARGV.index('--data-dir'))
|
|
358
|
+
KairosMcp.data_dir = File.expand_path(ARGV[idx + 1])
|
|
359
|
+
ARGV.delete_at(idx + 1)
|
|
360
|
+
ARGV.delete_at(idx)
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
action = ARGV.shift
|
|
364
|
+
case action
|
|
365
|
+
when 'scan'
|
|
366
|
+
require 'json'
|
|
367
|
+
require 'yaml'
|
|
368
|
+
require 'fileutils'
|
|
369
|
+
|
|
370
|
+
session = (i = ARGV.index('--session')) ? ARGV[i + 1] : nil
|
|
371
|
+
source = (i = ARGV.index('--source')) ? ARGV[i + 1] : 'session_end_hook'
|
|
372
|
+
|
|
373
|
+
syn_lib = File.join(KairosMcp.skillsets_dir, 'synoptis', 'lib')
|
|
374
|
+
unless File.directory?(syn_lib)
|
|
375
|
+
puts JSON.generate(status: 'error', message: 'synoptis skillset not installed')
|
|
376
|
+
exit 0
|
|
377
|
+
end
|
|
378
|
+
$LOAD_PATH.unshift(syn_lib)
|
|
379
|
+
require 'synoptis/registry/file_registry'
|
|
380
|
+
require 'synoptis/constitutive/subject_ref'
|
|
381
|
+
require 'synoptis/constitutive/proposal_criterion'
|
|
382
|
+
require 'synoptis/constitutive/attestation_chain'
|
|
383
|
+
|
|
384
|
+
context_dir = KairosMcp.context_dir
|
|
385
|
+
session ||= if Dir.exist?(context_dir)
|
|
386
|
+
Dir.children(context_dir)
|
|
387
|
+
.select { |c| File.directory?(File.join(context_dir, c)) }
|
|
388
|
+
.max_by { |c| File.mtime(File.join(context_dir, c)) }
|
|
389
|
+
end
|
|
390
|
+
unless session
|
|
391
|
+
puts JSON.generate(status: 'error', message: 'no session found to scan')
|
|
392
|
+
exit 0
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
cfg_path = File.join(KairosMcp.skillsets_dir, 'synoptis', 'config', 'synoptis.yml')
|
|
396
|
+
const_cfg = (File.exist?(cfg_path) ? (YAML.safe_load(File.read(cfg_path)) || {}) : {})['constitutive'] || {}
|
|
397
|
+
|
|
398
|
+
data_dir = File.join(KairosMcp.data_dir, 'synoptis_data')
|
|
399
|
+
FileUtils.mkdir_p(data_dir)
|
|
400
|
+
registry = Synoptis::Registry::FileRegistry.new(data_dir: data_dir)
|
|
401
|
+
chain = Synoptis::Constitutive::AttestationChain.new(registry: registry)
|
|
402
|
+
criterion = Synoptis::Constitutive::ProposalCriterion.new(
|
|
403
|
+
context_dir: context_dir,
|
|
404
|
+
judgment_types: const_cfg['judgment_types'] ||
|
|
405
|
+
Synoptis::Constitutive::ProposalCriterion::DEFAULT_JUDGMENT_TYPES,
|
|
406
|
+
preview_chars: const_cfg['preview_chars'] ||
|
|
407
|
+
Synoptis::Constitutive::ProposalCriterion::DEFAULT_PREVIEW_CHARS
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
proposals = criterion.propose(session_id: session)
|
|
411
|
+
chain.append_trigger(surfaced_count: proposals.length, source: source)
|
|
412
|
+
puts JSON.generate(
|
|
413
|
+
status: 'proposed',
|
|
414
|
+
session_id: session,
|
|
415
|
+
trigger_source: source,
|
|
416
|
+
surfaced_count: proposals.length,
|
|
417
|
+
proposals: proposals
|
|
418
|
+
)
|
|
419
|
+
exit 0
|
|
420
|
+
else
|
|
421
|
+
warn "Unknown attestation action: #{action.inspect}"
|
|
422
|
+
warn 'Usage: kairos-chain attestation scan [--session SID] [--source SRC] [--data-dir DIR]'
|
|
423
|
+
exit 1
|
|
424
|
+
end
|
|
425
|
+
|
|
346
426
|
when 'mode'
|
|
347
427
|
ARGV.shift # Remove 'mode' from ARGV
|
|
348
428
|
|
|
@@ -556,10 +636,22 @@ OptionParser.new do |opts|
|
|
|
556
636
|
options[:port] = port
|
|
557
637
|
end
|
|
558
638
|
|
|
559
|
-
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|
|
|
560
640
|
options[:host] = host
|
|
561
641
|
end
|
|
562
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
|
+
|
|
563
655
|
opts.on('--init-admin', 'Generate initial admin token and exit') do
|
|
564
656
|
options[:init_admin] = true
|
|
565
657
|
end
|
|
@@ -635,6 +727,72 @@ if options[:version]
|
|
|
635
727
|
exit
|
|
636
728
|
end
|
|
637
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
|
+
|
|
638
796
|
# Handle --init-admin
|
|
639
797
|
if options[:init_admin]
|
|
640
798
|
require 'kairos_mcp/auth/token_store'
|
|
@@ -654,6 +812,13 @@ if options[:init_admin]
|
|
|
654
812
|
store_path = File.join(KairosMcp.data_dir, store_path)
|
|
655
813
|
end
|
|
656
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
|
+
|
|
657
822
|
store = KairosMcp::Auth::TokenStore.create(
|
|
658
823
|
backend: http_config['token_backend'],
|
|
659
824
|
store_path: store_path
|
|
@@ -710,9 +875,9 @@ if options[:init_admin]
|
|
|
710
875
|
$stderr.puts " {"
|
|
711
876
|
$stderr.puts " \"mcpServers\": {"
|
|
712
877
|
$stderr.puts " \"kairos\": {"
|
|
713
|
-
$stderr.puts " \"url\": \"
|
|
878
|
+
$stderr.puts " \"url\": \"#{admin_scheme}://localhost:#{options[:port] || 8080}/mcp\","
|
|
714
879
|
$stderr.puts " \"headers\": {"
|
|
715
|
-
$stderr.puts " \"Authorization\": \"Bearer #{result['raw_token']}\""
|
|
880
|
+
$stderr.puts " \"Authorization\": \"Bearer #{result[:raw_token] || result['raw_token']}\""
|
|
716
881
|
$stderr.puts " }"
|
|
717
882
|
$stderr.puts " }"
|
|
718
883
|
$stderr.puts " }"
|
|
@@ -737,7 +902,8 @@ if options[:http]
|
|
|
737
902
|
server = KairosMcp::HttpServer.new(
|
|
738
903
|
port: options[:port],
|
|
739
904
|
host: options[:host],
|
|
740
|
-
token_store_path: options[:token_store]
|
|
905
|
+
token_store_path: options[:token_store],
|
|
906
|
+
tls: options[:tls]
|
|
741
907
|
)
|
|
742
908
|
server.run
|
|
743
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
|
|