kairos-chain 2.9.0 → 2.10.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.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -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 +51 -14
  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 +35 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f00cda5bb11b581435b296790b3cac3b49a1919f7280897b545b23ed2ead72a9
4
- data.tar.gz: cb213562d6fa26ee5cec11ee68f2242bff54ae72e23d51457a9beee11ece5fbd
3
+ metadata.gz: 22a10126d8c47cb81f6bf99b4c223d6458031d8af00192a25a90d150b05d514d
4
+ data.tar.gz: c36cae83262c20b11e32d226a8129ad8adbcc23da2f5ddecbb31b088db4029fd
5
5
  SHA512:
6
- metadata.gz: 7b53f2bde852c3a0476509819b31cea297904c9515cddf438b0922d1efcf8c7f4eaa2a9b1762d39ba2e8b2f9057287354e946bca8bc9654459b92ef0389a8323
7
- data.tar.gz: 832dcf7703ce7cbfcf035684841e7922370430725e412044b4427e87353459e87984b122717966b3c1aca1e5b874b2cf1fffc2b391844b3e9636ff6a0267e7c8
6
+ metadata.gz: 7ff3830bd7d998a9cbc35e97da26aeed80028836705872a1bedbb5d73dcff5feaae64e193cb1a56d7f54f25e6b0bd546984f73a50594138368f1e29539a356e2
7
+ data.tar.gz: 9d0db4ba9b32462a7aa6c66f5bc1bc15e35e3816573e1a4f39911738b48c0845894acd3ebae1cb3d4d5dcbfd08d55733d9eebe6d3d2372fbf036ef84f208afc6
data/CHANGELOG.md CHANGED
@@ -4,6 +4,39 @@ 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.0] - 2026-03-18
8
+
9
+ ### Added
10
+
11
+ - **Autonomos SkillSet** (`autonomos` v0.1.0): New opt-in SkillSet for autonomous project execution via OODA cycles with human-in-the-loop safety.
12
+ - `autonomos_cycle`: Single-cycle observe → orient → decide pass. Returns structured proposal with gap analysis, autoexec-compatible task JSON, and complexity-driven deliberation hints.
13
+ - `autonomos_reflect`: Post-execution reflection with L2 context save and two-phase chain recording (intent + outcome). Regex-based evaluation with human feedback correction.
14
+ - `autonomos_status`: Cycle history viewer for mandate and cycle state inspection.
15
+ - `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).
16
+ - **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.
17
+ - **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.
18
+ - **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.
19
+ - **Chain recording**: Two-phase commit (intent at decide, outcome at reflect) — constitutive, not evidential (Proposition 5).
20
+ - Bundled L1 knowledge: `autonomos_guide` (usage guide with goal convention, cycle states, continuous mode, related L1 knowledge references)
21
+ - Hard dependency on `autoexec` SkillSet
22
+ - 90 unit tests, 203 assertions
23
+
24
+ - **Review Discipline L1 Knowledge** (`review_discipline`): Codified countermeasures for LLM-common cognitive biases discovered during 6 rounds of multi-LLM review triangulation.
25
+ - 3 bias patterns: Caller-side bias, Fix-what-was-flagged bias, Mock fidelity bias
26
+ - Per-bias checklists for systematic review
27
+ - Multi-LLM review workflow (v0.1 manual)
28
+
29
+ ### Fixed
30
+
31
+ - **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.
32
+ - **Autonomos storage_path API mismatch**: `KairosMcp.kairos_dir` (nonexistent) → `KairosMcp.data_dir` (correct API). Previously fell back to `Dir.pwd/.kairos` accidentally.
33
+ - **Autonomos save_context return unchecked**: `Reflector.save_to_l2` now checks `save_context` return value for `{ success: false }` and returns nil on failure.
34
+ - **Autonomos load_l2_context key/order**: Fixed to use `sessions.first[:session_id]` (newest-first from ContextManager).
35
+ - **Autonomos loop detection number bypass**: Gap descriptions now number-normalized (`gsub(/\d+/, 'N')`) before comparison to prevent interpolated counts from defeating detection.
36
+ - **Autonomos orphan cycle on loop termination**: Mandate state (last_cycle_id, recent_gaps) saved BEFORE loop detection check so terminate_loop sees correct state.
37
+
38
+ ---
39
+
7
40
  ## [2.9.0] - 2026-03-14
8
41
 
9
42
  ### 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
 
@@ -45,17 +45,24 @@ module KairosMcp
45
45
 
46
46
  @port = port || http_config['port'] || DEFAULT_PORT
47
47
  @host = host || http_config['host'] || DEFAULT_HOST
48
+
49
+ # SkillSets must load BEFORE TokenStore.create so that plugins
50
+ # (e.g. Multiuser) can register alternative backends first.
51
+ eager_load_skillsets
52
+
48
53
  store_path = token_store_path || http_config['token_store']
49
54
  if store_path && !File.absolute_path?(store_path)
50
55
  store_path = File.join(KairosMcp.data_dir, store_path)
51
56
  end
52
- @token_store = Auth::TokenStore.new(store_path)
57
+
58
+ @token_store = Auth::TokenStore.create(
59
+ backend: http_config['token_backend'],
60
+ store_path: store_path
61
+ )
53
62
  @authenticator = Auth::Authenticator.new(@token_store)
54
63
  @admin_router = Admin::Router.new(token_store: @token_store, authenticator: @authenticator)
55
64
  @meeting_router = MeetingRouter.new
56
- @place_router = nil # Initialized lazily via meeting_place_start tool
57
-
58
- eager_load_skillsets
65
+ @place_router = nil
59
66
  end
60
67
 
61
68
  # Load SkillSets at startup so /meeting/* endpoints work immediately.
@@ -69,9 +76,11 @@ module KairosMcp
69
76
 
70
77
  # Start the HTTP server with Puma
71
78
  def run
79
+ KairosMcp.http_server = self
72
80
  check_dependencies!
73
81
  check_tokens!
74
82
  check_version_mismatch
83
+ auto_start_meeting_place
75
84
 
76
85
  app = build_rack_app
77
86
  server = self
@@ -82,6 +91,7 @@ module KairosMcp
82
91
  log "Health check: GET /health"
83
92
  log "Admin UI: GET /admin"
84
93
  log "MMP P2P: /meeting/v1/*"
94
+ log "Place API: /place/v1/*" if @place_router
85
95
 
86
96
  require 'puma'
87
97
  require 'puma/configuration'
@@ -162,7 +172,8 @@ module KairosMcp
162
172
  server: 'kairos-chain',
163
173
  version: KairosMcp::VERSION,
164
174
  transport: 'streamable-http',
165
- tokens_configured: !@token_store.empty?
175
+ tokens_configured: !@token_store.empty?,
176
+ place_started: !@place_router.nil?
166
177
  }
167
178
 
168
179
  [200, JSON_HEADERS, [body.to_json]]
@@ -216,15 +227,20 @@ module KairosMcp
216
227
  @place_router.call(env)
217
228
  end
218
229
 
219
- # Start the Meeting Place (called by meeting_place_start tool)
220
- def start_place(identity:, trust_anchor_client: nil)
230
+ # Start the Meeting Place (called by meeting_place_start tool or auto-start)
231
+ #
232
+ # @param hestia_config [Hash, nil] Full Hestia config hash. PlaceRouter
233
+ # expects the full config (it accesses config['meeting_place'] internally).
234
+ # When nil, PlaceRouter falls back to ::Hestia.load_config.
235
+ def start_place(identity:, trust_anchor_client: nil, hestia_config: nil)
221
236
  require 'hestia'
222
- @place_router = ::Hestia::PlaceRouter.new
223
- @place_router.start(
237
+ router = ::Hestia::PlaceRouter.new(config: hestia_config)
238
+ router.start(
224
239
  identity: identity,
225
240
  session_store: @meeting_router.session_store,
226
241
  trust_anchor_client: trust_anchor_client
227
242
  )
243
+ @place_router = router
228
244
  end
229
245
 
230
246
  # -----------------------------------------------------------------------
@@ -237,6 +253,29 @@ module KairosMcp
237
253
 
238
254
  private
239
255
 
256
+ # Auto-start Meeting Place if hestia.yml has meeting_place.enabled: true
257
+ def auto_start_meeting_place
258
+ require 'hestia'
259
+ hestia_config = ::Hestia.load_config
260
+ return unless hestia_config.dig('meeting_place', 'enabled')
261
+
262
+ mmp_config = ::MMP.load_config rescue nil
263
+ return unless mmp_config&.dig('enabled')
264
+
265
+ identity = ::MMP::Identity.new(config: mmp_config)
266
+ trust_anchor = nil
267
+ if hestia_config.dig('trust_anchor', 'record_registrations')
268
+ trust_anchor = ::Hestia.chain_client(config: hestia_config.dig('chain'))
269
+ end
270
+
271
+ start_place(identity: identity, trust_anchor_client: trust_anchor, hestia_config: hestia_config)
272
+ log "Meeting Place auto-started (config: meeting_place.enabled = true)"
273
+ rescue LoadError => e
274
+ $stderr.puts "[HttpServer] Meeting Place auto-start skipped: Hestia SkillSet not available (#{e.message})"
275
+ rescue StandardError => e
276
+ $stderr.puts "[HttpServer] Meeting Place auto-start failed: #{e.message}"
277
+ end
278
+
240
279
  def check_dependencies!
241
280
  begin
242
281
  require 'puma'
@@ -261,11 +300,9 @@ module KairosMcp
261
300
  def check_tokens!
262
301
  if @token_store.empty?
263
302
  $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
303
+ [INFO] Local dev mode: no tokens configured.
304
+ MCP endpoint accepts unauthenticated requests as local owner.
305
+ For production, generate a token with: kairos-chain --init-admin
269
306
 
270
307
  MSG
271
308
  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)
@@ -39,12 +39,12 @@ module KairosMcp
39
39
  '.pdf' => 'application/pdf'
40
40
  }.freeze
41
41
 
42
- def initialize
42
+ def initialize(user_context: nil)
43
43
  @skills_dir = KairosMcp.skills_dir
44
- @knowledge_dir = KairosMcp.knowledge_dir
45
- @context_dir = KairosMcp.context_dir
46
- @knowledge_provider = KnowledgeProvider.new(@knowledge_dir, vector_search_enabled: false)
47
- @context_manager = ContextManager.new(@context_dir)
44
+ @knowledge_dir = KairosMcp.knowledge_dir(user_context: user_context)
45
+ @context_dir = KairosMcp.context_dir(user_context: user_context)
46
+ @knowledge_provider = KnowledgeProvider.new(@knowledge_dir, vector_search_enabled: false, user_context: user_context)
47
+ @context_manager = ContextManager.new(@context_dir, user_context: user_context)
48
48
  end
49
49
 
50
50
  # List all resources with optional filtering
@@ -334,7 +334,7 @@ module KairosMcp
334
334
 
335
335
  # Check if auto-commit should be triggered
336
336
  if SkillsConfig.state_commit_auto_enabled?
337
- service = StateCommit::CommitService.new
337
+ service = StateCommit::CommitService.new(user_context: Thread.current[:kairos_user_context])
338
338
  service.check_and_auto_commit
339
339
  end
340
340
  rescue StandardError => e