kairos-chain 2.9.0 → 2.10.1
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 +42 -0
- data/bin/kairos-chain +99 -31
- data/lib/kairos_mcp/admin/router.rb +12 -8
- data/lib/kairos_mcp/auth/authenticator.rb +9 -0
- data/lib/kairos_mcp/auth/token_store.rb +28 -0
- data/lib/kairos_mcp/context_manager.rb +3 -2
- data/lib/kairos_mcp/http_server.rb +89 -20
- data/lib/kairos_mcp/knowledge_provider.rb +4 -3
- data/lib/kairos_mcp/protocol.rb +38 -4
- data/lib/kairos_mcp/resource_registry.rb +5 -5
- data/lib/kairos_mcp/safe_evolver.rb +1 -1
- data/lib/kairos_mcp/safety.rb +44 -15
- data/lib/kairos_mcp/skillset.rb +17 -0
- data/lib/kairos_mcp/skillset_manager.rb +80 -0
- data/lib/kairos_mcp/state_commit/commit_service.rb +3 -2
- data/lib/kairos_mcp/state_commit/manifest_builder.rb +4 -3
- data/lib/kairos_mcp/storage/backend.rb +21 -0
- data/lib/kairos_mcp/tool_registry.rb +44 -0
- data/lib/kairos_mcp/tools/context_create_subdir.rb +1 -1
- data/lib/kairos_mcp/tools/context_save.rb +1 -1
- data/lib/kairos_mcp/tools/knowledge_get.rb +1 -1
- data/lib/kairos_mcp/tools/knowledge_list.rb +1 -1
- data/lib/kairos_mcp/tools/knowledge_update.rb +1 -1
- data/lib/kairos_mcp/tools/resource_list.rb +1 -1
- data/lib/kairos_mcp/tools/resource_read.rb +1 -1
- data/lib/kairos_mcp/tools/skills_audit.rb +14 -14
- data/lib/kairos_mcp/tools/skills_promote.rb +5 -5
- data/lib/kairos_mcp/tools/state_commit.rb +1 -1
- data/lib/kairos_mcp/tools/state_history.rb +1 -1
- data/lib/kairos_mcp/tools/state_status.rb +1 -1
- data/lib/kairos_mcp/tools/token_manage.rb +6 -1
- data/lib/kairos_mcp/version.rb +1 -1
- data/lib/kairos_mcp.rb +57 -6
- data/templates/knowledge/multi_agent_design_workflow/multi_agent_design_workflow.md +40 -4
- data/templates/knowledge/multi_agent_design_workflow_jp/multi_agent_design_workflow_jp.md +39 -4
- data/templates/knowledge/review_discipline/review_discipline.md +69 -0
- data/templates/skillsets/autonomos/config/autonomos.yml +6 -0
- data/templates/skillsets/autonomos/knowledge/autonomos_guide/autonomos_guide.md +272 -0
- data/templates/skillsets/autonomos/lib/autonomos/cycle_store.rb +173 -0
- data/templates/skillsets/autonomos/lib/autonomos/mandate.rb +207 -0
- data/templates/skillsets/autonomos/lib/autonomos/ooda.rb +333 -0
- data/templates/skillsets/autonomos/lib/autonomos/reflector.rb +197 -0
- data/templates/skillsets/autonomos/lib/autonomos.rb +119 -0
- data/templates/skillsets/autonomos/skillset.json +27 -0
- data/templates/skillsets/autonomos/test/test_autonomos.rb +1449 -0
- data/templates/skillsets/autonomos/tools/autonomos_cycle.rb +146 -0
- data/templates/skillsets/autonomos/tools/autonomos_loop.rb +532 -0
- data/templates/skillsets/autonomos/tools/autonomos_reflect.rb +99 -0
- data/templates/skillsets/autonomos/tools/autonomos_status.rb +204 -0
- data/templates/skillsets/hestia/config/hestia.yml +14 -0
- data/templates/skillsets/hestia/lib/hestia/place_router.rb +168 -8
- data/templates/skillsets/hestia/lib/hestia/skill_board.rb +415 -21
- data/templates/skillsets/hestia/tools/meeting_place_start.rb +44 -4
- data/templates/skillsets/mmp/lib/mmp/identity.rb +7 -4
- data/templates/skillsets/mmp/skillset.json +4 -1
- data/templates/skillsets/mmp/tools/meeting_acquire_skill.rb +139 -17
- data/templates/skillsets/mmp/tools/meeting_browse.rb +132 -0
- data/templates/skillsets/mmp/tools/meeting_connect.rb +133 -16
- data/templates/skillsets/mmp/tools/meeting_deposit.rb +191 -0
- data/templates/skillsets/mmp/tools/meeting_federate.rb +280 -0
- data/templates/skillsets/mmp/tools/meeting_get_skill_details.rb +9 -7
- data/templates/skillsets/multiuser/config/multiuser.yml +16 -0
- data/templates/skillsets/multiuser/lib/multiuser/authorization_gate.rb +43 -0
- data/templates/skillsets/multiuser/lib/multiuser/pg_backend.rb +338 -0
- data/templates/skillsets/multiuser/lib/multiuser/pg_connection_pool.rb +98 -0
- data/templates/skillsets/multiuser/lib/multiuser/request_filter.rb +22 -0
- data/templates/skillsets/multiuser/lib/multiuser/tenant_manager.rb +176 -0
- data/templates/skillsets/multiuser/lib/multiuser/tenant_token_store.rb +194 -0
- data/templates/skillsets/multiuser/lib/multiuser/user_registry.rb +125 -0
- data/templates/skillsets/multiuser/lib/multiuser.rb +154 -0
- data/templates/skillsets/multiuser/migrations/001_public_schema.sql +49 -0
- data/templates/skillsets/multiuser/migrations/002_tenant_template.sql +45 -0
- data/templates/skillsets/multiuser/skillset.json +14 -0
- data/templates/skillsets/multiuser/tools/multiuser_migrate.rb +88 -0
- data/templates/skillsets/multiuser/tools/multiuser_status.rb +90 -0
- data/templates/skillsets/multiuser/tools/multiuser_user_manage.rb +112 -0
- data/templates/skillsets/synoptis/lib/synoptis/proof_envelope.rb +6 -0
- data/templates/skillsets/synoptis/lib/synoptis/trust_scorer.rb +29 -4
- data/templates/skillsets/synoptis/tools/attestation_issue.rb +5 -4
- metadata +34 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a238ca634bf5f063383755826516315843ec93b5bbf6a1d9740a76ef367e9450
|
|
4
|
+
data.tar.gz: f9f3098d7c48248fae20ebaaba7d50353a70792e33dde79c512370017d78ef24
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b6360bcfaaf7fd8b3920e5b99992cf515cf1448257a29128fc6e33804c872e73339a558829b0ab5891f4699f4117aea71f1fbcfe0ca39d76171de6397138aecb
|
|
7
|
+
data.tar.gz: 7243622826d6e9b994f29ccb463a6f2b1af78e7e9f3605fc57e247eee1368bcb7ca929406cfa9443228ea3cad0d985f7e78f0def14388b5b03a947c4a1c4b6e7
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,48 @@ 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
|
+
## [2.10.1] - 2026-03-19
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **Streamable HTTP session management**: MCP clients (Claude Code, Cursor) could not discover tools over HTTP transport because each request created a new `Protocol` instance, losing the `@initialized` state from the handshake. Fixed with stateless design: `initialize` returns `Mcp-Session-Id` header (spec compliance), subsequent requests auto-initialize the Protocol internally. No server-side session store needed — per-request Bearer token authentication is sufficient.
|
|
12
|
+
- **`DELETE /mcp` endpoint**: Added support for MCP session termination requests (returns 204, no-op in stateless mode).
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## [2.10.0] - 2026-03-18
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **Autonomos SkillSet** (`autonomos` v0.1.0): New opt-in SkillSet for autonomous project execution via OODA cycles with human-in-the-loop safety.
|
|
21
|
+
- `autonomos_cycle`: Single-cycle observe → orient → decide pass. Returns structured proposal with gap analysis, autoexec-compatible task JSON, and complexity-driven deliberation hints.
|
|
22
|
+
- `autonomos_reflect`: Post-execution reflection with L2 context save and two-phase chain recording (intent + outcome). Regex-based evaluation with human feedback correction.
|
|
23
|
+
- `autonomos_status`: Cycle history viewer for mandate and cycle state inspection.
|
|
24
|
+
- `autonomos_loop`: Continuous mode via mandate-based pre-authorization. Multi-cycle execution with risk budget gates (`low`/`medium`), goal hash verification, checkpoint system (1-3 cycle intervals), loop detection (number-normalized string comparison), and error threshold (2 consecutive).
|
|
25
|
+
- **L2-first goal loading**: Goals loaded from L2 context (newest session first) with L1 knowledge fallback. Supports `type: autonomos_goal` frontmatter and checklist-based gap identification.
|
|
26
|
+
- **Complexity-driven deliberation**: Proposals include `complexity_hint` (`low`/`medium`/`high`) with signals (`high_risk`, `many_gaps`, `design_scope`) to guide LLM escalation to persona assembly review.
|
|
27
|
+
- **Safety model**: PID-based cycle lock, inherited autoexec safety (risk classification, L0 deny-list, hash-locked plans), goal hash verification per cycle, no L0 modification capability.
|
|
28
|
+
- **Chain recording**: Two-phase commit (intent at decide, outcome at reflect) — constitutive, not evidential (Proposition 5).
|
|
29
|
+
- Bundled L1 knowledge: `autonomos_guide` (usage guide with goal convention, cycle states, continuous mode, related L1 knowledge references)
|
|
30
|
+
- Hard dependency on `autoexec` SkillSet
|
|
31
|
+
- 90 unit tests, 203 assertions
|
|
32
|
+
|
|
33
|
+
- **Review Discipline L1 Knowledge** (`review_discipline`): Codified countermeasures for LLM-common cognitive biases discovered during 6 rounds of multi-LLM review triangulation.
|
|
34
|
+
- 3 bias patterns: Caller-side bias, Fix-what-was-flagged bias, Mock fidelity bias
|
|
35
|
+
- Per-bias checklists for systematic review
|
|
36
|
+
- Multi-LLM review workflow (v0.1 manual)
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- **Autonomos checkpoint resume infinite loop**: `checkpoint_due?` re-evaluated immediately on resume when `cycles_completed % checkpoint_every == 0` still true. Fixed with `resuming_from_pause` flag that skips checkpoint evaluation once after resume.
|
|
41
|
+
- **Autonomos storage_path API mismatch**: `KairosMcp.kairos_dir` (nonexistent) → `KairosMcp.data_dir` (correct API). Previously fell back to `Dir.pwd/.kairos` accidentally.
|
|
42
|
+
- **Autonomos save_context return unchecked**: `Reflector.save_to_l2` now checks `save_context` return value for `{ success: false }` and returns nil on failure.
|
|
43
|
+
- **Autonomos load_l2_context key/order**: Fixed to use `sessions.first[:session_id]` (newest-first from ContextManager).
|
|
44
|
+
- **Autonomos loop detection number bypass**: Gap descriptions now number-normalized (`gsub(/\d+/, 'N')`) before comparison to prevent interpolated counts from defeating detection.
|
|
45
|
+
- **Autonomos orphan cycle on loop termination**: Mandate state (last_cycle_id, recent_gaps) saved BEFORE loop detection check so terminate_loop sees correct state.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
7
49
|
## [2.9.0] - 2026-03-14
|
|
8
50
|
|
|
9
51
|
### Added
|
data/bin/kairos-chain
CHANGED
|
@@ -119,6 +119,30 @@ when 'skillset'
|
|
|
119
119
|
exit 1
|
|
120
120
|
end
|
|
121
121
|
|
|
122
|
+
when 'upgrade'
|
|
123
|
+
apply = ARGV.delete('--apply')
|
|
124
|
+
upgrades = manager.upgrade_check
|
|
125
|
+
|
|
126
|
+
if upgrades.empty?
|
|
127
|
+
puts "All SkillSets are up to date."
|
|
128
|
+
elsif apply
|
|
129
|
+
results = manager.upgrade_apply
|
|
130
|
+
results.each do |r|
|
|
131
|
+
puts "Updated #{r[:name]}: v#{r[:from]} -> v#{r[:to]} (#{r[:files_updated]} files)"
|
|
132
|
+
end
|
|
133
|
+
puts "Done. #{results.size} SkillSet(s) upgraded."
|
|
134
|
+
else
|
|
135
|
+
puts "SkillSet upgrades available:"
|
|
136
|
+
puts ""
|
|
137
|
+
upgrades.each do |u|
|
|
138
|
+
label = u[:version_bump] ? "v#{u[:installed_version]} -> v#{u[:available_version]}" : "files changed"
|
|
139
|
+
puts " #{u[:name]}: #{label}"
|
|
140
|
+
u[:changed_files].each { |f| puts " Modified: #{f}" }
|
|
141
|
+
puts ""
|
|
142
|
+
end
|
|
143
|
+
puts "Run 'kairos-chain skillset upgrade --apply' to apply updates."
|
|
144
|
+
end
|
|
145
|
+
|
|
122
146
|
when 'remove'
|
|
123
147
|
name = ARGV.shift
|
|
124
148
|
unless name
|
|
@@ -316,6 +340,14 @@ OptionParser.new do |opts|
|
|
|
316
340
|
options[:init_admin] = true
|
|
317
341
|
end
|
|
318
342
|
|
|
343
|
+
opts.on('--quiet', 'Suppress token output (for scripted/Docker usage)') do
|
|
344
|
+
options[:quiet] = true
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
opts.on('--token-output-file PATH', 'Write raw admin token to file (mode 0600)') do |path|
|
|
348
|
+
options[:token_output_file] = path
|
|
349
|
+
end
|
|
350
|
+
|
|
319
351
|
opts.on('--token-store PATH', 'Path to token store file') do |path|
|
|
320
352
|
options[:token_store] = path
|
|
321
353
|
end
|
|
@@ -374,8 +406,26 @@ end
|
|
|
374
406
|
# Handle --init-admin
|
|
375
407
|
if options[:init_admin]
|
|
376
408
|
require 'kairos_mcp/auth/token_store'
|
|
409
|
+
require 'kairos_mcp/skills_config'
|
|
410
|
+
|
|
411
|
+
# Load SkillSets first so registered backends (e.g. Multiuser/PostgreSQL) are available
|
|
412
|
+
begin
|
|
413
|
+
require 'kairos_mcp/skillset_manager'
|
|
414
|
+
KairosMcp::SkillSetManager.new.enabled_skillsets.each(&:load!)
|
|
415
|
+
rescue StandardError => e
|
|
416
|
+
$stderr.puts "[init-admin] SkillSet load: #{e.message}"
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
http_config = KairosMcp::SkillsConfig.load['http'] || {}
|
|
420
|
+
store_path = options[:token_store] || http_config['token_store']
|
|
421
|
+
if store_path && !File.absolute_path?(store_path)
|
|
422
|
+
store_path = File.join(KairosMcp.data_dir, store_path)
|
|
423
|
+
end
|
|
377
424
|
|
|
378
|
-
store = KairosMcp::Auth::TokenStore.
|
|
425
|
+
store = KairosMcp::Auth::TokenStore.create(
|
|
426
|
+
backend: http_config['token_backend'],
|
|
427
|
+
store_path: store_path
|
|
428
|
+
)
|
|
379
429
|
|
|
380
430
|
if !store.empty?
|
|
381
431
|
$stderr.puts "[WARNING] Active tokens already exist."
|
|
@@ -385,40 +435,58 @@ if options[:init_admin]
|
|
|
385
435
|
store.list.each do |t|
|
|
386
436
|
$stderr.puts " - #{t[:user]} (#{t[:role]}, expires: #{t[:expires_at] || 'never'})"
|
|
387
437
|
end
|
|
388
|
-
|
|
389
|
-
$
|
|
390
|
-
|
|
391
|
-
|
|
438
|
+
|
|
439
|
+
if $stdin.tty?
|
|
440
|
+
$stderr.puts ""
|
|
441
|
+
$stderr.puts "Proceed anyway? (y/N)"
|
|
442
|
+
answer = $stdin.gets&.strip
|
|
443
|
+
exit unless answer&.downcase == 'y'
|
|
444
|
+
else
|
|
445
|
+
$stderr.puts ""
|
|
446
|
+
$stderr.puts "[init-admin] Non-interactive mode: skipping confirmation."
|
|
447
|
+
end
|
|
392
448
|
end
|
|
393
449
|
|
|
394
450
|
result = store.create(user: 'admin', role: 'owner', issued_by: 'system')
|
|
395
451
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
puts ""
|
|
409
|
-
puts "
|
|
410
|
-
puts "
|
|
411
|
-
puts "
|
|
412
|
-
puts "
|
|
413
|
-
puts "
|
|
414
|
-
puts "
|
|
415
|
-
puts "
|
|
416
|
-
puts "
|
|
417
|
-
puts "
|
|
418
|
-
puts "
|
|
419
|
-
puts "
|
|
420
|
-
puts ""
|
|
421
|
-
puts "
|
|
452
|
+
if options[:token_output_file]
|
|
453
|
+
require 'fileutils'
|
|
454
|
+
FileUtils.mkdir_p(File.dirname(options[:token_output_file]))
|
|
455
|
+
File.write(options[:token_output_file], result[:raw_token] || result['raw_token'])
|
|
456
|
+
File.chmod(0600, options[:token_output_file])
|
|
457
|
+
$stderr.puts "[init-admin] Token written to: #{options[:token_output_file]}"
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
if options[:quiet]
|
|
461
|
+
exit
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
$stderr.puts ""
|
|
465
|
+
$stderr.puts "=" * 60
|
|
466
|
+
$stderr.puts " KairosChain Admin Token Generated"
|
|
467
|
+
$stderr.puts "=" * 60
|
|
468
|
+
$stderr.puts ""
|
|
469
|
+
$stderr.puts " Token: #{result[:raw_token] || result['raw_token']}"
|
|
470
|
+
$stderr.puts " User: #{result[:user] || result['user']}"
|
|
471
|
+
$stderr.puts " Role: #{result[:role] || result['role']}"
|
|
472
|
+
$stderr.puts " Expires: #{result[:expires_at] || result['expires_at'] || 'never'}"
|
|
473
|
+
$stderr.puts ""
|
|
474
|
+
$stderr.puts " IMPORTANT: Store this token securely."
|
|
475
|
+
$stderr.puts " It will NOT be shown again."
|
|
476
|
+
$stderr.puts ""
|
|
477
|
+
$stderr.puts " Configure in Cursor mcp.json:"
|
|
478
|
+
$stderr.puts " {"
|
|
479
|
+
$stderr.puts " \"mcpServers\": {"
|
|
480
|
+
$stderr.puts " \"kairos\": {"
|
|
481
|
+
$stderr.puts " \"url\": \"http://localhost:#{options[:port] || 8080}/mcp\","
|
|
482
|
+
$stderr.puts " \"headers\": {"
|
|
483
|
+
$stderr.puts " \"Authorization\": \"Bearer #{result['raw_token']}\""
|
|
484
|
+
$stderr.puts " }"
|
|
485
|
+
$stderr.puts " }"
|
|
486
|
+
$stderr.puts " }"
|
|
487
|
+
$stderr.puts " }"
|
|
488
|
+
$stderr.puts ""
|
|
489
|
+
$stderr.puts "=" * 60
|
|
422
490
|
exit
|
|
423
491
|
end
|
|
424
492
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'uri'
|
|
5
5
|
require_relative 'helpers'
|
|
6
|
+
require_relative '../protocol'
|
|
6
7
|
|
|
7
8
|
module KairosMcp
|
|
8
9
|
module Admin
|
|
@@ -75,7 +76,7 @@ module KairosMcp
|
|
|
75
76
|
return redirect_with_flash('/admin/login', 'Admin access requires owner role.')
|
|
76
77
|
end
|
|
77
78
|
|
|
78
|
-
@current_user = user_info
|
|
79
|
+
@current_user = Protocol.apply_all_filters(user_info)
|
|
79
80
|
@csrf_token = session[:csrf_token]
|
|
80
81
|
|
|
81
82
|
# CSRF check for POST requests
|
|
@@ -83,12 +84,15 @@ module KairosMcp
|
|
|
83
84
|
return html_response(403, render('layout', content: '<p>CSRF validation failed. Please try again.</p>'))
|
|
84
85
|
end
|
|
85
86
|
|
|
86
|
-
# Authenticated routes
|
|
87
|
+
# Authenticated routes — set thread-scoped user context for PgBackend
|
|
88
|
+
Thread.current[:kairos_user_context] = @current_user
|
|
87
89
|
route(method, path, env)
|
|
88
90
|
rescue StandardError => e
|
|
89
91
|
$stderr.puts "[ADMIN ERROR] #{e.message}"
|
|
90
92
|
$stderr.puts e.backtrace.first(5).join("\n")
|
|
91
93
|
html_response(500, render('_error', layout: true, error: e.message))
|
|
94
|
+
ensure
|
|
95
|
+
Thread.current[:kairos_user_context] = nil
|
|
92
96
|
end
|
|
93
97
|
|
|
94
98
|
private
|
|
@@ -407,7 +411,7 @@ module KairosMcp
|
|
|
407
411
|
|
|
408
412
|
def handle_knowledge_detail_partial(name)
|
|
409
413
|
require_relative '../knowledge_provider'
|
|
410
|
-
provider = KnowledgeProvider.new
|
|
414
|
+
provider = KnowledgeProvider.new(nil, user_context: @current_user)
|
|
411
415
|
entry = provider.get(name)
|
|
412
416
|
|
|
413
417
|
if entry
|
|
@@ -428,7 +432,7 @@ module KairosMcp
|
|
|
428
432
|
|
|
429
433
|
def handle_context_list_partial(session_id)
|
|
430
434
|
require_relative '../context_manager'
|
|
431
|
-
manager = ContextManager.new
|
|
435
|
+
manager = ContextManager.new(nil, user_context: @current_user)
|
|
432
436
|
contexts = manager.list_contexts_in_session(session_id)
|
|
433
437
|
html_response(200, render_partial('_context_list',
|
|
434
438
|
session_id: session_id,
|
|
@@ -437,7 +441,7 @@ module KairosMcp
|
|
|
437
441
|
|
|
438
442
|
def handle_context_detail_partial(session_id, name)
|
|
439
443
|
require_relative '../context_manager'
|
|
440
|
-
manager = ContextManager.new
|
|
444
|
+
manager = ContextManager.new(nil, user_context: @current_user)
|
|
441
445
|
entry = manager.get_context(session_id, name)
|
|
442
446
|
|
|
443
447
|
if entry
|
|
@@ -486,7 +490,7 @@ module KairosMcp
|
|
|
486
490
|
|
|
487
491
|
def fetch_knowledge_list(search: nil)
|
|
488
492
|
require_relative '../knowledge_provider'
|
|
489
|
-
provider = KnowledgeProvider.new
|
|
493
|
+
provider = KnowledgeProvider.new(nil, user_context: @current_user)
|
|
490
494
|
if search && !search.empty?
|
|
491
495
|
provider.search(search)
|
|
492
496
|
else
|
|
@@ -506,7 +510,7 @@ module KairosMcp
|
|
|
506
510
|
|
|
507
511
|
def fetch_state_status
|
|
508
512
|
require_relative '../state_commit/commit_service'
|
|
509
|
-
service = StateCommit::CommitService.new
|
|
513
|
+
service = StateCommit::CommitService.new(user_context: @current_user)
|
|
510
514
|
service.status
|
|
511
515
|
rescue StandardError
|
|
512
516
|
{ enabled: false, has_changes: false,
|
|
@@ -515,7 +519,7 @@ module KairosMcp
|
|
|
515
519
|
|
|
516
520
|
def fetch_context_sessions
|
|
517
521
|
require_relative '../context_manager'
|
|
518
|
-
manager = ContextManager.new
|
|
522
|
+
manager = ContextManager.new(nil, user_context: @current_user)
|
|
519
523
|
manager.list_sessions
|
|
520
524
|
rescue StandardError
|
|
521
525
|
[]
|
|
@@ -36,6 +36,15 @@ module KairosMcp
|
|
|
36
36
|
# @param env [Hash] Rack environment hash
|
|
37
37
|
# @return [AuthResult] Result with success/failure details
|
|
38
38
|
def authenticate!(env)
|
|
39
|
+
# Local dev mode: when no tokens are configured, allow unauthenticated
|
|
40
|
+
# access with a default owner context. This makes local testing seamless.
|
|
41
|
+
if @token_store.empty?
|
|
42
|
+
return AuthResult.new(
|
|
43
|
+
success: true,
|
|
44
|
+
user_context: { user: 'local', role: 'owner', local_dev: true }
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
39
48
|
raw_token = extract_bearer_token(env)
|
|
40
49
|
|
|
41
50
|
unless raw_token
|
|
@@ -40,6 +40,34 @@ module KairosMcp
|
|
|
40
40
|
TOKEN_PREFIX = 'kc_'
|
|
41
41
|
DEFAULT_EXPIRY_DAYS = 90
|
|
42
42
|
|
|
43
|
+
# =====================================================================
|
|
44
|
+
# SkillSet TokenStore Registry
|
|
45
|
+
# =====================================================================
|
|
46
|
+
|
|
47
|
+
@registry = {}
|
|
48
|
+
|
|
49
|
+
# Register a named TokenStore backend (e.g. 'postgresql')
|
|
50
|
+
def self.register(name, klass)
|
|
51
|
+
@registry[name.to_s] = klass
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.unregister(name)
|
|
55
|
+
@registry.delete(name.to_s)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Factory: create a TokenStore based on config.
|
|
59
|
+
# If a SkillSet has registered a backend matching config[:backend],
|
|
60
|
+
# use that; otherwise fall back to file-based store.
|
|
61
|
+
def self.create(config = {})
|
|
62
|
+
backend = config[:backend]&.to_s
|
|
63
|
+
if backend && @registry.key?(backend)
|
|
64
|
+
return @registry[backend].new(config[backend.to_sym] || {})
|
|
65
|
+
end
|
|
66
|
+
new(config[:store_path])
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# =====================================================================
|
|
70
|
+
|
|
43
71
|
attr_reader :store_path
|
|
44
72
|
|
|
45
73
|
def initialize(store_path = nil)
|
|
@@ -13,9 +13,10 @@ module KairosMcp
|
|
|
13
13
|
# - Session-based organization
|
|
14
14
|
#
|
|
15
15
|
class ContextManager
|
|
16
|
-
def initialize(context_dir = nil)
|
|
17
|
-
context_dir ||= KairosMcp.context_dir
|
|
16
|
+
def initialize(context_dir = nil, user_context: nil)
|
|
17
|
+
context_dir ||= KairosMcp.context_dir(user_context: user_context)
|
|
18
18
|
@context_dir = context_dir
|
|
19
|
+
@user_context = user_context
|
|
19
20
|
FileUtils.mkdir_p(@context_dir)
|
|
20
21
|
end
|
|
21
22
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'yaml'
|
|
5
|
+
require 'securerandom'
|
|
5
6
|
require_relative 'protocol'
|
|
6
7
|
require_relative 'version'
|
|
7
8
|
require_relative '../kairos_mcp'
|
|
@@ -45,17 +46,24 @@ module KairosMcp
|
|
|
45
46
|
|
|
46
47
|
@port = port || http_config['port'] || DEFAULT_PORT
|
|
47
48
|
@host = host || http_config['host'] || DEFAULT_HOST
|
|
49
|
+
|
|
50
|
+
# SkillSets must load BEFORE TokenStore.create so that plugins
|
|
51
|
+
# (e.g. Multiuser) can register alternative backends first.
|
|
52
|
+
eager_load_skillsets
|
|
53
|
+
|
|
48
54
|
store_path = token_store_path || http_config['token_store']
|
|
49
55
|
if store_path && !File.absolute_path?(store_path)
|
|
50
56
|
store_path = File.join(KairosMcp.data_dir, store_path)
|
|
51
57
|
end
|
|
52
|
-
|
|
58
|
+
|
|
59
|
+
@token_store = Auth::TokenStore.create(
|
|
60
|
+
backend: http_config['token_backend'],
|
|
61
|
+
store_path: store_path
|
|
62
|
+
)
|
|
53
63
|
@authenticator = Auth::Authenticator.new(@token_store)
|
|
54
64
|
@admin_router = Admin::Router.new(token_store: @token_store, authenticator: @authenticator)
|
|
55
65
|
@meeting_router = MeetingRouter.new
|
|
56
|
-
@place_router = nil
|
|
57
|
-
|
|
58
|
-
eager_load_skillsets
|
|
66
|
+
@place_router = nil
|
|
59
67
|
end
|
|
60
68
|
|
|
61
69
|
# Load SkillSets at startup so /meeting/* endpoints work immediately.
|
|
@@ -69,9 +77,11 @@ module KairosMcp
|
|
|
69
77
|
|
|
70
78
|
# Start the HTTP server with Puma
|
|
71
79
|
def run
|
|
80
|
+
KairosMcp.http_server = self
|
|
72
81
|
check_dependencies!
|
|
73
82
|
check_tokens!
|
|
74
83
|
check_version_mismatch
|
|
84
|
+
auto_start_meeting_place
|
|
75
85
|
|
|
76
86
|
app = build_rack_app
|
|
77
87
|
server = self
|
|
@@ -82,6 +92,7 @@ module KairosMcp
|
|
|
82
92
|
log "Health check: GET /health"
|
|
83
93
|
log "Admin UI: GET /admin"
|
|
84
94
|
log "MMP P2P: /meeting/v1/*"
|
|
95
|
+
log "Place API: /place/v1/*" if @place_router
|
|
85
96
|
|
|
86
97
|
require 'puma'
|
|
87
98
|
require 'puma/configuration'
|
|
@@ -141,6 +152,9 @@ module KairosMcp
|
|
|
141
152
|
server.handle_health
|
|
142
153
|
when ['POST', '/mcp']
|
|
143
154
|
server.handle_mcp(env)
|
|
155
|
+
when ['DELETE', '/mcp']
|
|
156
|
+
# Streamable HTTP spec: session termination (no-op in stateless mode)
|
|
157
|
+
[204, {}, []]
|
|
144
158
|
when ['GET', '/mcp']
|
|
145
159
|
# Streamable HTTP spec: GET /mcp for SSE streaming
|
|
146
160
|
server.json_response(501, error: 'not_implemented',
|
|
@@ -162,7 +176,8 @@ module KairosMcp
|
|
|
162
176
|
server: 'kairos-chain',
|
|
163
177
|
version: KairosMcp::VERSION,
|
|
164
178
|
transport: 'streamable-http',
|
|
165
|
-
tokens_configured: !@token_store.empty
|
|
179
|
+
tokens_configured: !@token_store.empty?,
|
|
180
|
+
place_started: !@place_router.nil?
|
|
166
181
|
}
|
|
167
182
|
|
|
168
183
|
[200, JSON_HEADERS, [body.to_json]]
|
|
@@ -187,16 +202,44 @@ module KairosMcp
|
|
|
187
202
|
message: 'Content-Type must be application/json')
|
|
188
203
|
end
|
|
189
204
|
|
|
190
|
-
# 3.
|
|
205
|
+
# 3. Parse request to detect method
|
|
206
|
+
parsed = JSON.parse(body)
|
|
207
|
+
method = parsed['method']
|
|
208
|
+
|
|
209
|
+
# 4. Process MCP message with user context
|
|
210
|
+
#
|
|
211
|
+
# Stateless design: each request creates a fresh Protocol instance.
|
|
212
|
+
# For non-initialize methods, we auto-initialize the Protocol first
|
|
213
|
+
# so that @initialized=true and tools are available.
|
|
214
|
+
# See L1 knowledge: kairoschain_operations "Streamable HTTP Transport: Stateless Design"
|
|
191
215
|
user_context = auth_result.user_context
|
|
192
216
|
protocol = Protocol.new(user_context: user_context)
|
|
193
|
-
response = protocol.handle_message(body)
|
|
194
217
|
|
|
195
|
-
if
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
218
|
+
if method == 'initialize'
|
|
219
|
+
# First request in MCP handshake — process normally, return Mcp-Session-Id
|
|
220
|
+
response = protocol.handle_message(body)
|
|
221
|
+
session_id = SecureRandom.hex(32)
|
|
222
|
+
headers = JSON_HEADERS.merge('Mcp-Session-Id' => session_id)
|
|
223
|
+
[200, headers, [response.to_json]]
|
|
224
|
+
elsif method == 'initialized'
|
|
225
|
+
# Notification — no response body needed
|
|
199
226
|
[204, {}, []]
|
|
227
|
+
else
|
|
228
|
+
# For tools/list, tools/call, etc.: auto-initialize the Protocol
|
|
229
|
+
# so it doesn't reject the request due to @initialized being false.
|
|
230
|
+
protocol.handle_message({
|
|
231
|
+
'jsonrpc' => '2.0', 'id' => '_init', 'method' => 'initialize',
|
|
232
|
+
'params' => { 'protocolVersion' => Protocol::HTTP_PROTOCOL_VERSION,
|
|
233
|
+
'capabilities' => {},
|
|
234
|
+
'clientInfo' => { 'name' => 'http-stateless', 'version' => '1.0' } }
|
|
235
|
+
}.to_json)
|
|
236
|
+
|
|
237
|
+
response = protocol.handle_message(body)
|
|
238
|
+
if response
|
|
239
|
+
[200, JSON_HEADERS, [response.to_json]]
|
|
240
|
+
else
|
|
241
|
+
[204, {}, []]
|
|
242
|
+
end
|
|
200
243
|
end
|
|
201
244
|
rescue JSON::ParserError
|
|
202
245
|
json_response(400, error: 'bad_request', message: 'Invalid JSON in request body')
|
|
@@ -216,15 +259,20 @@ module KairosMcp
|
|
|
216
259
|
@place_router.call(env)
|
|
217
260
|
end
|
|
218
261
|
|
|
219
|
-
# Start the Meeting Place (called by meeting_place_start tool)
|
|
220
|
-
|
|
262
|
+
# Start the Meeting Place (called by meeting_place_start tool or auto-start)
|
|
263
|
+
#
|
|
264
|
+
# @param hestia_config [Hash, nil] Full Hestia config hash. PlaceRouter
|
|
265
|
+
# expects the full config (it accesses config['meeting_place'] internally).
|
|
266
|
+
# When nil, PlaceRouter falls back to ::Hestia.load_config.
|
|
267
|
+
def start_place(identity:, trust_anchor_client: nil, hestia_config: nil)
|
|
221
268
|
require 'hestia'
|
|
222
|
-
|
|
223
|
-
|
|
269
|
+
router = ::Hestia::PlaceRouter.new(config: hestia_config)
|
|
270
|
+
router.start(
|
|
224
271
|
identity: identity,
|
|
225
272
|
session_store: @meeting_router.session_store,
|
|
226
273
|
trust_anchor_client: trust_anchor_client
|
|
227
274
|
)
|
|
275
|
+
@place_router = router
|
|
228
276
|
end
|
|
229
277
|
|
|
230
278
|
# -----------------------------------------------------------------------
|
|
@@ -237,6 +285,29 @@ module KairosMcp
|
|
|
237
285
|
|
|
238
286
|
private
|
|
239
287
|
|
|
288
|
+
# Auto-start Meeting Place if hestia.yml has meeting_place.enabled: true
|
|
289
|
+
def auto_start_meeting_place
|
|
290
|
+
require 'hestia'
|
|
291
|
+
hestia_config = ::Hestia.load_config
|
|
292
|
+
return unless hestia_config.dig('meeting_place', 'enabled')
|
|
293
|
+
|
|
294
|
+
mmp_config = ::MMP.load_config rescue nil
|
|
295
|
+
return unless mmp_config&.dig('enabled')
|
|
296
|
+
|
|
297
|
+
identity = ::MMP::Identity.new(config: mmp_config)
|
|
298
|
+
trust_anchor = nil
|
|
299
|
+
if hestia_config.dig('trust_anchor', 'record_registrations')
|
|
300
|
+
trust_anchor = ::Hestia.chain_client(config: hestia_config.dig('chain'))
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
start_place(identity: identity, trust_anchor_client: trust_anchor, hestia_config: hestia_config)
|
|
304
|
+
log "Meeting Place auto-started (config: meeting_place.enabled = true)"
|
|
305
|
+
rescue LoadError => e
|
|
306
|
+
$stderr.puts "[HttpServer] Meeting Place auto-start skipped: Hestia SkillSet not available (#{e.message})"
|
|
307
|
+
rescue StandardError => e
|
|
308
|
+
$stderr.puts "[HttpServer] Meeting Place auto-start failed: #{e.message}"
|
|
309
|
+
end
|
|
310
|
+
|
|
240
311
|
def check_dependencies!
|
|
241
312
|
begin
|
|
242
313
|
require 'puma'
|
|
@@ -261,11 +332,9 @@ module KairosMcp
|
|
|
261
332
|
def check_tokens!
|
|
262
333
|
if @token_store.empty?
|
|
263
334
|
$stderr.puts <<~MSG
|
|
264
|
-
[
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
Generate an admin token with:
|
|
268
|
-
kairos-chain --init-admin
|
|
335
|
+
[INFO] Local dev mode: no tokens configured.
|
|
336
|
+
MCP endpoint accepts unauthenticated requests as local owner.
|
|
337
|
+
For production, generate a token with: kairos-chain --init-admin
|
|
269
338
|
|
|
270
339
|
MSG
|
|
271
340
|
end
|
|
@@ -31,9 +31,10 @@ module KairosMcp
|
|
|
31
31
|
# @param knowledge_dir [String] Path to knowledge directory
|
|
32
32
|
# @param vector_search_enabled [Boolean] Enable vector search
|
|
33
33
|
# @param storage_backend [Storage::Backend, nil] Storage backend to use
|
|
34
|
-
def initialize(knowledge_dir = nil, vector_search_enabled: true, storage_backend: nil)
|
|
35
|
-
knowledge_dir ||= KairosMcp.knowledge_dir
|
|
34
|
+
def initialize(knowledge_dir = nil, vector_search_enabled: true, storage_backend: nil, user_context: nil)
|
|
35
|
+
knowledge_dir ||= KairosMcp.knowledge_dir(user_context: user_context)
|
|
36
36
|
@knowledge_dir = knowledge_dir
|
|
37
|
+
@user_context = user_context
|
|
37
38
|
@vector_search_enabled = vector_search_enabled
|
|
38
39
|
@storage_backend = storage_backend
|
|
39
40
|
@vector_search = nil
|
|
@@ -658,7 +659,7 @@ module KairosMcp
|
|
|
658
659
|
|
|
659
660
|
# Check if auto-commit should be triggered
|
|
660
661
|
if SkillsConfig.state_commit_auto_enabled?
|
|
661
|
-
service = StateCommit::CommitService.new
|
|
662
|
+
service = StateCommit::CommitService.new(user_context: @user_context)
|
|
662
663
|
service.check_and_auto_commit
|
|
663
664
|
end
|
|
664
665
|
rescue StandardError => e
|
data/lib/kairos_mcp/protocol.rb
CHANGED
|
@@ -10,11 +10,42 @@ module KairosMcp
|
|
|
10
10
|
STDIO_PROTOCOL_VERSION = '2024-11-05'
|
|
11
11
|
HTTP_PROTOCOL_VERSION = '2025-03-26'
|
|
12
12
|
|
|
13
|
+
# =========================================================================
|
|
14
|
+
# SkillSet Filter Registry
|
|
15
|
+
# =========================================================================
|
|
16
|
+
|
|
17
|
+
@filters = {}
|
|
18
|
+
@filter_mutex = Mutex.new
|
|
19
|
+
|
|
20
|
+
# Register a named request filter.
|
|
21
|
+
# Filters transform user_context before it reaches ToolRegistry.
|
|
22
|
+
def self.register_filter(name, &block)
|
|
23
|
+
@filter_mutex.synchronize { @filters[name.to_sym] = block }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.unregister_filter(name)
|
|
27
|
+
@filter_mutex.synchronize { @filters.delete(name.to_sym) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Apply all registered filters to user_context in registration order
|
|
31
|
+
def self.apply_all_filters(user_context)
|
|
32
|
+
@filter_mutex.synchronize { @filters.values.dup }.reduce(user_context) do |ctx, filter|
|
|
33
|
+
filter.call(ctx)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# For testing only
|
|
38
|
+
def self.clear_filters!
|
|
39
|
+
@filter_mutex.synchronize { @filters = {} }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# =========================================================================
|
|
43
|
+
|
|
13
44
|
# @param user_context [Hash, nil] Authenticated user info from HTTP mode
|
|
14
45
|
# { user: "name", role: "owner"|"member"|"guest", ... }
|
|
15
46
|
def initialize(user_context: nil)
|
|
16
|
-
@user_context = user_context
|
|
17
|
-
@tool_registry = ToolRegistry.new(user_context: user_context)
|
|
47
|
+
@user_context = self.class.apply_all_filters(user_context)
|
|
48
|
+
@tool_registry = ToolRegistry.new(user_context: @user_context)
|
|
18
49
|
@initialized = false
|
|
19
50
|
end
|
|
20
51
|
|
|
@@ -145,12 +176,15 @@ module KairosMcp
|
|
|
145
176
|
def handle_tools_call(params)
|
|
146
177
|
name = params['name']
|
|
147
178
|
arguments = params['arguments'] || {}
|
|
148
|
-
|
|
179
|
+
|
|
180
|
+
Thread.current[:kairos_user_context] = @user_context
|
|
149
181
|
content = @tool_registry.call_tool(name, arguments)
|
|
150
|
-
|
|
182
|
+
|
|
151
183
|
{
|
|
152
184
|
content: content
|
|
153
185
|
}
|
|
186
|
+
ensure
|
|
187
|
+
Thread.current[:kairos_user_context] = nil
|
|
154
188
|
end
|
|
155
189
|
|
|
156
190
|
def format_response(id, result)
|