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.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -0
  3. data/bin/kairos-chain +99 -31
  4. data/lib/kairos_mcp/admin/router.rb +12 -8
  5. data/lib/kairos_mcp/auth/authenticator.rb +9 -0
  6. data/lib/kairos_mcp/auth/token_store.rb +28 -0
  7. data/lib/kairos_mcp/context_manager.rb +3 -2
  8. data/lib/kairos_mcp/http_server.rb +89 -20
  9. data/lib/kairos_mcp/knowledge_provider.rb +4 -3
  10. data/lib/kairos_mcp/protocol.rb +38 -4
  11. data/lib/kairos_mcp/resource_registry.rb +5 -5
  12. data/lib/kairos_mcp/safe_evolver.rb +1 -1
  13. data/lib/kairos_mcp/safety.rb +44 -15
  14. data/lib/kairos_mcp/skillset.rb +17 -0
  15. data/lib/kairos_mcp/skillset_manager.rb +80 -0
  16. data/lib/kairos_mcp/state_commit/commit_service.rb +3 -2
  17. data/lib/kairos_mcp/state_commit/manifest_builder.rb +4 -3
  18. data/lib/kairos_mcp/storage/backend.rb +21 -0
  19. data/lib/kairos_mcp/tool_registry.rb +44 -0
  20. data/lib/kairos_mcp/tools/context_create_subdir.rb +1 -1
  21. data/lib/kairos_mcp/tools/context_save.rb +1 -1
  22. data/lib/kairos_mcp/tools/knowledge_get.rb +1 -1
  23. data/lib/kairos_mcp/tools/knowledge_list.rb +1 -1
  24. data/lib/kairos_mcp/tools/knowledge_update.rb +1 -1
  25. data/lib/kairos_mcp/tools/resource_list.rb +1 -1
  26. data/lib/kairos_mcp/tools/resource_read.rb +1 -1
  27. data/lib/kairos_mcp/tools/skills_audit.rb +14 -14
  28. data/lib/kairos_mcp/tools/skills_promote.rb +5 -5
  29. data/lib/kairos_mcp/tools/state_commit.rb +1 -1
  30. data/lib/kairos_mcp/tools/state_history.rb +1 -1
  31. data/lib/kairos_mcp/tools/state_status.rb +1 -1
  32. data/lib/kairos_mcp/tools/token_manage.rb +6 -1
  33. data/lib/kairos_mcp/version.rb +1 -1
  34. data/lib/kairos_mcp.rb +57 -6
  35. data/templates/knowledge/multi_agent_design_workflow/multi_agent_design_workflow.md +40 -4
  36. data/templates/knowledge/multi_agent_design_workflow_jp/multi_agent_design_workflow_jp.md +39 -4
  37. data/templates/knowledge/review_discipline/review_discipline.md +69 -0
  38. data/templates/skillsets/autonomos/config/autonomos.yml +6 -0
  39. data/templates/skillsets/autonomos/knowledge/autonomos_guide/autonomos_guide.md +272 -0
  40. data/templates/skillsets/autonomos/lib/autonomos/cycle_store.rb +173 -0
  41. data/templates/skillsets/autonomos/lib/autonomos/mandate.rb +207 -0
  42. data/templates/skillsets/autonomos/lib/autonomos/ooda.rb +333 -0
  43. data/templates/skillsets/autonomos/lib/autonomos/reflector.rb +197 -0
  44. data/templates/skillsets/autonomos/lib/autonomos.rb +119 -0
  45. data/templates/skillsets/autonomos/skillset.json +27 -0
  46. data/templates/skillsets/autonomos/test/test_autonomos.rb +1449 -0
  47. data/templates/skillsets/autonomos/tools/autonomos_cycle.rb +146 -0
  48. data/templates/skillsets/autonomos/tools/autonomos_loop.rb +532 -0
  49. data/templates/skillsets/autonomos/tools/autonomos_reflect.rb +99 -0
  50. data/templates/skillsets/autonomos/tools/autonomos_status.rb +204 -0
  51. data/templates/skillsets/hestia/config/hestia.yml +14 -0
  52. data/templates/skillsets/hestia/lib/hestia/place_router.rb +168 -8
  53. data/templates/skillsets/hestia/lib/hestia/skill_board.rb +415 -21
  54. data/templates/skillsets/hestia/tools/meeting_place_start.rb +44 -4
  55. data/templates/skillsets/mmp/lib/mmp/identity.rb +7 -4
  56. data/templates/skillsets/mmp/skillset.json +4 -1
  57. data/templates/skillsets/mmp/tools/meeting_acquire_skill.rb +139 -17
  58. data/templates/skillsets/mmp/tools/meeting_browse.rb +132 -0
  59. data/templates/skillsets/mmp/tools/meeting_connect.rb +133 -16
  60. data/templates/skillsets/mmp/tools/meeting_deposit.rb +191 -0
  61. data/templates/skillsets/mmp/tools/meeting_federate.rb +280 -0
  62. data/templates/skillsets/mmp/tools/meeting_get_skill_details.rb +9 -7
  63. data/templates/skillsets/multiuser/config/multiuser.yml +16 -0
  64. data/templates/skillsets/multiuser/lib/multiuser/authorization_gate.rb +43 -0
  65. data/templates/skillsets/multiuser/lib/multiuser/pg_backend.rb +338 -0
  66. data/templates/skillsets/multiuser/lib/multiuser/pg_connection_pool.rb +98 -0
  67. data/templates/skillsets/multiuser/lib/multiuser/request_filter.rb +22 -0
  68. data/templates/skillsets/multiuser/lib/multiuser/tenant_manager.rb +176 -0
  69. data/templates/skillsets/multiuser/lib/multiuser/tenant_token_store.rb +194 -0
  70. data/templates/skillsets/multiuser/lib/multiuser/user_registry.rb +125 -0
  71. data/templates/skillsets/multiuser/lib/multiuser.rb +154 -0
  72. data/templates/skillsets/multiuser/migrations/001_public_schema.sql +49 -0
  73. data/templates/skillsets/multiuser/migrations/002_tenant_template.sql +45 -0
  74. data/templates/skillsets/multiuser/skillset.json +14 -0
  75. data/templates/skillsets/multiuser/tools/multiuser_migrate.rb +88 -0
  76. data/templates/skillsets/multiuser/tools/multiuser_status.rb +90 -0
  77. data/templates/skillsets/multiuser/tools/multiuser_user_manage.rb +112 -0
  78. data/templates/skillsets/synoptis/lib/synoptis/proof_envelope.rb +6 -0
  79. data/templates/skillsets/synoptis/lib/synoptis/trust_scorer.rb +29 -4
  80. data/templates/skillsets/synoptis/tools/attestation_issue.rb +5 -4
  81. metadata +34 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f00cda5bb11b581435b296790b3cac3b49a1919f7280897b545b23ed2ead72a9
4
- data.tar.gz: cb213562d6fa26ee5cec11ee68f2242bff54ae72e23d51457a9beee11ece5fbd
3
+ metadata.gz: a238ca634bf5f063383755826516315843ec93b5bbf6a1d9740a76ef367e9450
4
+ data.tar.gz: f9f3098d7c48248fae20ebaaba7d50353a70792e33dde79c512370017d78ef24
5
5
  SHA512:
6
- metadata.gz: 7b53f2bde852c3a0476509819b31cea297904c9515cddf438b0922d1efcf8c7f4eaa2a9b1762d39ba2e8b2f9057287354e946bca8bc9654459b92ef0389a8323
7
- data.tar.gz: 832dcf7703ce7cbfcf035684841e7922370430725e412044b4427e87353459e87984b122717966b3c1aca1e5b874b2cf1fffc2b391844b3e9636ff6a0267e7c8
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.new(options[:token_store])
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
- $stderr.puts ""
389
- $stderr.puts "Proceed anyway? (y/N)"
390
- answer = $stdin.gets&.strip
391
- exit unless answer&.downcase == 'y'
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
- puts ""
397
- puts "=" * 60
398
- puts " KairosChain Admin Token Generated"
399
- puts "=" * 60
400
- puts ""
401
- puts " Token: #{result['raw_token']}"
402
- puts " User: #{result['user']}"
403
- puts " Role: #{result['role']}"
404
- puts " Expires: #{result['expires_at'] || 'never'}"
405
- puts ""
406
- puts " IMPORTANT: Store this token securely."
407
- puts " It will NOT be shown again."
408
- puts ""
409
- puts " Configure in Cursor mcp.json:"
410
- puts " {"
411
- puts " \"mcpServers\": {"
412
- puts " \"kairos\": {"
413
- puts " \"url\": \"http://localhost:#{options[:port] || 8080}/mcp\","
414
- puts " \"headers\": {"
415
- puts " \"Authorization\": \"Bearer #{result['raw_token']}\""
416
- puts " }"
417
- puts " }"
418
- puts " }"
419
- puts " }"
420
- puts ""
421
- puts "=" * 60
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
- @token_store = Auth::TokenStore.new(store_path)
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 # Initialized lazily via meeting_place_start tool
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. Process MCP message with user context
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 response
196
- [200, JSON_HEADERS, [response.to_json]]
197
- else
198
- # Some MCP messages (like 'initialized') return nil
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
- def start_place(identity:, trust_anchor_client: nil)
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
- @place_router = ::Hestia::PlaceRouter.new
223
- @place_router.start(
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
- [WARNING] No active tokens found.
265
- No clients will be able to connect without a valid token.
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
@@ -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)