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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +33 -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 +51 -14
- 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 +35 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 22a10126d8c47cb81f6bf99b4c223d6458031d8af00192a25a90d150b05d514d
|
|
4
|
+
data.tar.gz: c36cae83262c20b11e32d226a8129ad8adbcc23da2f5ddecbb31b088db4029fd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
|
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
223
|
-
|
|
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
|
-
[
|
|
265
|
-
|
|
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
|
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)
|
|
@@ -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
|