kairos-chain 1.0.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 +7 -0
- data/bin/kairos-chain +255 -0
- data/bin/kairos_mcp_server +12 -0
- data/lib/kairos-chain.rb +5 -0
- data/lib/kairos_mcp/action_log.rb +78 -0
- data/lib/kairos_mcp/admin/helpers.rb +250 -0
- data/lib/kairos_mcp/admin/router.rb +548 -0
- data/lib/kairos_mcp/admin/views/_error.erb +5 -0
- data/lib/kairos_mcp/admin/views/chain.erb +56 -0
- data/lib/kairos_mcp/admin/views/config.erb +118 -0
- data/lib/kairos_mcp/admin/views/context.erb +59 -0
- data/lib/kairos_mcp/admin/views/dashboard.erb +117 -0
- data/lib/kairos_mcp/admin/views/knowledge.erb +73 -0
- data/lib/kairos_mcp/admin/views/layout.erb +39 -0
- data/lib/kairos_mcp/admin/views/login.erb +37 -0
- data/lib/kairos_mcp/admin/views/partials/_chain_blocks.erb +64 -0
- data/lib/kairos_mcp/admin/views/partials/_chain_detail.erb +30 -0
- data/lib/kairos_mcp/admin/views/partials/_context_detail.erb +43 -0
- data/lib/kairos_mcp/admin/views/partials/_context_list.erb +37 -0
- data/lib/kairos_mcp/admin/views/partials/_knowledge_detail.erb +44 -0
- data/lib/kairos_mcp/admin/views/partials/_skill_detail.erb +37 -0
- data/lib/kairos_mcp/admin/views/partials/_token_list.erb +110 -0
- data/lib/kairos_mcp/admin/views/skills.erb +50 -0
- data/lib/kairos_mcp/admin/views/tokens.erb +46 -0
- data/lib/kairos_mcp/anthropic_skill_parser.rb +218 -0
- data/lib/kairos_mcp/auth/authenticator.rb +99 -0
- data/lib/kairos_mcp/auth/token_store.rb +256 -0
- data/lib/kairos_mcp/config_merger.rb +142 -0
- data/lib/kairos_mcp/context_manager.rb +196 -0
- data/lib/kairos_mcp/dsl_skills_provider.rb +127 -0
- data/lib/kairos_mcp/http_server.rb +244 -0
- data/lib/kairos_mcp/initializer.rb +185 -0
- data/lib/kairos_mcp/kairos.rb +83 -0
- data/lib/kairos_mcp/kairos_chain/block.rb +58 -0
- data/lib/kairos_mcp/kairos_chain/chain.rb +120 -0
- data/lib/kairos_mcp/kairos_chain/merkle_tree.rb +88 -0
- data/lib/kairos_mcp/kairos_chain/skill_transition.rb +45 -0
- data/lib/kairos_mcp/knowledge_provider.rb +598 -0
- data/lib/kairos_mcp/layer_registry.rb +172 -0
- data/lib/kairos_mcp/protocol.rb +113 -0
- data/lib/kairos_mcp/resource_registry.rb +503 -0
- data/lib/kairos_mcp/safe_evolver.rb +345 -0
- data/lib/kairos_mcp/safety.rb +130 -0
- data/lib/kairos_mcp/server.rb +66 -0
- data/lib/kairos_mcp/skill_contexts.rb +145 -0
- data/lib/kairos_mcp/skill_tool_adapter.rb +66 -0
- data/lib/kairos_mcp/skills_ast.rb +55 -0
- data/lib/kairos_mcp/skills_config.rb +178 -0
- data/lib/kairos_mcp/skills_dsl.rb +143 -0
- data/lib/kairos_mcp/skills_parser.rb +100 -0
- data/lib/kairos_mcp/state_commit/commit_service.rb +264 -0
- data/lib/kairos_mcp/state_commit/diff_calculator.rb +309 -0
- data/lib/kairos_mcp/state_commit/manifest_builder.rb +205 -0
- data/lib/kairos_mcp/state_commit/pending_changes.rb +197 -0
- data/lib/kairos_mcp/state_commit/snapshot_manager.rb +197 -0
- data/lib/kairos_mcp/storage/backend.rb +179 -0
- data/lib/kairos_mcp/storage/exporter.rb +195 -0
- data/lib/kairos_mcp/storage/file_backend.rb +173 -0
- data/lib/kairos_mcp/storage/importer.rb +284 -0
- data/lib/kairos_mcp/storage/sqlite_backend.rb +389 -0
- data/lib/kairos_mcp/tool_registry.rb +123 -0
- data/lib/kairos_mcp/tools/base_tool.rb +87 -0
- data/lib/kairos_mcp/tools/chain_export.rb +105 -0
- data/lib/kairos_mcp/tools/chain_history.rb +213 -0
- data/lib/kairos_mcp/tools/chain_import.rb +269 -0
- data/lib/kairos_mcp/tools/chain_record.rb +61 -0
- data/lib/kairos_mcp/tools/chain_status.rb +68 -0
- data/lib/kairos_mcp/tools/chain_verify.rb +55 -0
- data/lib/kairos_mcp/tools/context_create_subdir.rb +80 -0
- data/lib/kairos_mcp/tools/context_save.rb +98 -0
- data/lib/kairos_mcp/tools/hello_world.rb +57 -0
- data/lib/kairos_mcp/tools/knowledge_get.rb +120 -0
- data/lib/kairos_mcp/tools/knowledge_list.rb +94 -0
- data/lib/kairos_mcp/tools/knowledge_update.rb +139 -0
- data/lib/kairos_mcp/tools/resource_list.rb +163 -0
- data/lib/kairos_mcp/tools/resource_read.rb +187 -0
- data/lib/kairos_mcp/tools/skills_audit.rb +1014 -0
- data/lib/kairos_mcp/tools/skills_dsl_get.rb +73 -0
- data/lib/kairos_mcp/tools/skills_dsl_list.rb +90 -0
- data/lib/kairos_mcp/tools/skills_evolve.rb +168 -0
- data/lib/kairos_mcp/tools/skills_get.rb +71 -0
- data/lib/kairos_mcp/tools/skills_list.rb +65 -0
- data/lib/kairos_mcp/tools/skills_promote.rb +624 -0
- data/lib/kairos_mcp/tools/skills_rollback.rb +141 -0
- data/lib/kairos_mcp/tools/state_commit.rb +169 -0
- data/lib/kairos_mcp/tools/state_history.rb +207 -0
- data/lib/kairos_mcp/tools/state_status.rb +121 -0
- data/lib/kairos_mcp/tools/system_upgrade.rb +648 -0
- data/lib/kairos_mcp/tools/token_manage.rb +249 -0
- data/lib/kairos_mcp/tools/tool_guide.rb +834 -0
- data/lib/kairos_mcp/upgrade_analyzer.rb +263 -0
- data/lib/kairos_mcp/vector_search/base.rb +82 -0
- data/lib/kairos_mcp/vector_search/fallback_search.rb +109 -0
- data/lib/kairos_mcp/vector_search/provider.rb +99 -0
- data/lib/kairos_mcp/vector_search/semantic_search.rb +286 -0
- data/lib/kairos_mcp/version.rb +4 -0
- data/lib/kairos_mcp/version_manager.rb +96 -0
- data/lib/kairos_mcp.rb +219 -0
- data/templates/config/safety.yml +13 -0
- data/templates/config/tool_metadata.yml +37 -0
- data/templates/context/.gitkeep +0 -0
- data/templates/knowledge/.gitkeep +0 -0
- data/templates/knowledge/example_knowledge/example_knowledge.md +38 -0
- data/templates/knowledge/example_knowledge/references/README.md +15 -0
- data/templates/knowledge/example_knowledge/scripts/example_script.sh +8 -0
- data/templates/knowledge/kairoschain_design/kairoschain_design.md +176 -0
- data/templates/knowledge/kairoschain_design_jp/kairoschain_design_jp.md +176 -0
- data/templates/knowledge/kairoschain_faq/kairoschain_faq.md +1492 -0
- data/templates/knowledge/kairoschain_faq_jp/kairoschain_faq_jp.md +1423 -0
- data/templates/knowledge/kairoschain_operations/kairoschain_operations.md +260 -0
- data/templates/knowledge/kairoschain_operations_jp/kairoschain_operations_jp.md +299 -0
- data/templates/knowledge/kairoschain_philosophy/kairoschain_philosophy.md +193 -0
- data/templates/knowledge/kairoschain_philosophy_jp/kairoschain_philosophy_jp.md +193 -0
- data/templates/knowledge/kairoschain_setup/kairoschain_setup.md +1356 -0
- data/templates/knowledge/kairoschain_setup_jp/kairoschain_setup_jp.md +1340 -0
- data/templates/knowledge/kairoschain_usage/kairoschain_usage.md +381 -0
- data/templates/knowledge/kairoschain_usage_jp/kairoschain_usage_jp.md +381 -0
- data/templates/knowledge/l1_health_guide/l1_health_guide.md +220 -0
- data/templates/knowledge/layer_placement_guide/layer_placement_guide.md +179 -0
- data/templates/knowledge/mcp_to_saas_development_workflow/mcp_to_saas_development_workflow.md +301 -0
- data/templates/knowledge/persona_definitions/persona_definitions.md +270 -0
- data/templates/skills/config.yml +207 -0
- data/templates/skills/kairos.md +401 -0
- data/templates/skills/kairos.rb +760 -0
- data/templates/skills/versions/.gitkeep +0 -0
- data/templates/storage/embeddings/knowledge/.gitkeep +0 -0
- data/templates/storage/embeddings/skills/.gitkeep +0 -0
- metadata +206 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 89223ad42654fe812b9219cfeb4d136101e3cf88054cd0dc8f99999c993af43a
|
|
4
|
+
data.tar.gz: c75b155784af0ce1782c67bb5f925b83428ffbab463150567a0bdb04798ced66
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 007d915cbb44e3ed2834a1c176dbf28ea9631b7ef2ea88c934e20926f3dace4058f4ea393327dbf5a0923070b5ac35526a88dcae9d25f22f761eef3d38ba40d3
|
|
7
|
+
data.tar.gz: baf5e843902f5e7d3b09dc62087c1dbadd1cda17055400d5a6acc66de9b9df3431507aa551eb61984596b2d84c15c7d23af54cfd4d4ece6387ee42810862aac2
|
data/bin/kairos-chain
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# KairosChain - Memory-driven agent framework
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# kairos-chain # stdio mode (default, for Cursor local)
|
|
8
|
+
# kairos-chain --http # Streamable HTTP mode (remote access)
|
|
9
|
+
# kairos-chain --http --port 9090
|
|
10
|
+
# kairos-chain --init-admin # Generate initial admin token
|
|
11
|
+
# kairos-chain init [DIR] # Initialize data directory with templates
|
|
12
|
+
# kairos-chain init --data-dir /path # Initialize at specific path
|
|
13
|
+
# kairos-chain upgrade # Preview template migrations after gem update
|
|
14
|
+
# kairos-chain upgrade --apply # Apply template migrations
|
|
15
|
+
#
|
|
16
|
+
# Data directory resolution (priority order):
|
|
17
|
+
# 1. --data-dir CLI option
|
|
18
|
+
# 2. KAIROS_DATA_DIR environment variable
|
|
19
|
+
# 3. .kairos/ in the current working directory
|
|
20
|
+
#
|
|
21
|
+
# Streamable HTTP mode requires: gem install puma rack
|
|
22
|
+
|
|
23
|
+
require 'optparse'
|
|
24
|
+
|
|
25
|
+
# Check for subcommands first (before OptionParser)
|
|
26
|
+
case ARGV[0]
|
|
27
|
+
when 'init'
|
|
28
|
+
ARGV.shift # Remove 'init' from ARGV
|
|
29
|
+
|
|
30
|
+
init_options = {}
|
|
31
|
+
OptionParser.new do |opts|
|
|
32
|
+
opts.banner = "Usage: kairos-chain init [options] [DIR]"
|
|
33
|
+
|
|
34
|
+
opts.on('--data-dir DIR', 'Data directory path (default: .kairos/)') do |dir|
|
|
35
|
+
init_options[:data_dir] = dir
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
opts.on('-h', '--help', 'Show help') do
|
|
39
|
+
puts opts
|
|
40
|
+
exit
|
|
41
|
+
end
|
|
42
|
+
end.parse!
|
|
43
|
+
|
|
44
|
+
# Remaining argument is the directory
|
|
45
|
+
init_dir = init_options[:data_dir] || ARGV.shift
|
|
46
|
+
|
|
47
|
+
# Setup load path and require entry point
|
|
48
|
+
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
|
49
|
+
require 'kairos_mcp'
|
|
50
|
+
|
|
51
|
+
# Set data_dir if specified, otherwise use default
|
|
52
|
+
if init_dir
|
|
53
|
+
KairosMcp.data_dir = File.expand_path(init_dir)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
require 'kairos_mcp/initializer'
|
|
57
|
+
KairosMcp::Initializer.run
|
|
58
|
+
exit
|
|
59
|
+
|
|
60
|
+
when 'upgrade'
|
|
61
|
+
ARGV.shift # Remove 'upgrade' from ARGV
|
|
62
|
+
|
|
63
|
+
upgrade_options = {}
|
|
64
|
+
OptionParser.new do |opts|
|
|
65
|
+
opts.banner = "Usage: kairos-chain upgrade [options]"
|
|
66
|
+
|
|
67
|
+
opts.on('--data-dir DIR', 'Data directory path (default: .kairos/)') do |dir|
|
|
68
|
+
upgrade_options[:data_dir] = dir
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
opts.on('--apply', 'Apply the upgrade (default: preview only)') do
|
|
72
|
+
upgrade_options[:apply] = true
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
opts.on('-h', '--help', 'Show help') do
|
|
76
|
+
puts opts
|
|
77
|
+
exit
|
|
78
|
+
end
|
|
79
|
+
end.parse!
|
|
80
|
+
|
|
81
|
+
# Setup load path and require entry point
|
|
82
|
+
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
|
83
|
+
require 'kairos_mcp'
|
|
84
|
+
|
|
85
|
+
# Set data_dir if specified
|
|
86
|
+
if upgrade_options[:data_dir]
|
|
87
|
+
KairosMcp.data_dir = File.expand_path(upgrade_options[:data_dir])
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
require 'kairos_mcp/upgrade_analyzer'
|
|
91
|
+
require 'kairos_mcp/config_merger'
|
|
92
|
+
require 'kairos_mcp/tools/system_upgrade'
|
|
93
|
+
|
|
94
|
+
# Create a minimal tool instance for CLI use
|
|
95
|
+
tool = KairosMcp::Tools::SystemUpgrade.new
|
|
96
|
+
command = upgrade_options[:apply] ? 'apply' : 'preview'
|
|
97
|
+
result = tool.call({ 'command' => command, 'approved' => upgrade_options[:apply] || false })
|
|
98
|
+
|
|
99
|
+
# Extract text from MCP response format
|
|
100
|
+
result.each do |content|
|
|
101
|
+
puts content[:text] if content[:type] == 'text'
|
|
102
|
+
end
|
|
103
|
+
exit
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Parse CLI options
|
|
107
|
+
options = {}
|
|
108
|
+
|
|
109
|
+
OptionParser.new do |opts|
|
|
110
|
+
opts.banner = "Usage: kairos-chain [options]"
|
|
111
|
+
|
|
112
|
+
opts.on('--data-dir DIR', 'Data directory path (default: .kairos/ or KAIROS_DATA_DIR)') do |dir|
|
|
113
|
+
options[:data_dir] = dir
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
opts.on('--http', 'Start in Streamable HTTP mode (default: stdio)') do
|
|
117
|
+
options[:http] = true
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
opts.on('--port PORT', Integer, 'HTTP port (default: 8080)') do |port|
|
|
121
|
+
options[:port] = port
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
opts.on('--host HOST', 'HTTP bind host (default: 0.0.0.0)') do |host|
|
|
125
|
+
options[:host] = host
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
opts.on('--init-admin', 'Generate initial admin token and exit') do
|
|
129
|
+
options[:init_admin] = true
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
opts.on('--token-store PATH', 'Path to token store file') do |path|
|
|
133
|
+
options[:token_store] = path
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
opts.on('-v', '--version', 'Show version') do
|
|
137
|
+
options[:version] = true
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
opts.on('-h', '--help', 'Show help') do
|
|
141
|
+
puts opts
|
|
142
|
+
puts ""
|
|
143
|
+
puts "Subcommands:"
|
|
144
|
+
puts " init [DIR] Initialize data directory with default templates"
|
|
145
|
+
puts " upgrade [--apply] Check/apply template migrations after gem update"
|
|
146
|
+
exit
|
|
147
|
+
end
|
|
148
|
+
end.parse!
|
|
149
|
+
|
|
150
|
+
# Setup bundler if Gemfile exists (enables optional gems)
|
|
151
|
+
gemfile_path = File.expand_path('../Gemfile', __dir__)
|
|
152
|
+
if File.exist?(gemfile_path)
|
|
153
|
+
ENV['BUNDLE_GEMFILE'] ||= gemfile_path
|
|
154
|
+
# Use local vendor/bundle if it exists
|
|
155
|
+
vendor_path = File.expand_path('../vendor/bundle', __dir__)
|
|
156
|
+
ENV['BUNDLE_PATH'] ||= vendor_path if File.directory?(vendor_path)
|
|
157
|
+
begin
|
|
158
|
+
require 'bundler/setup'
|
|
159
|
+
rescue Bundler::GemNotFound, LoadError
|
|
160
|
+
# Fall back to running without bundler if gems not installed
|
|
161
|
+
warn "[KairosChain] Bundler gems not found, running without optional dependencies"
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
|
166
|
+
|
|
167
|
+
# Load entry point
|
|
168
|
+
require 'kairos_mcp'
|
|
169
|
+
|
|
170
|
+
# Set data_dir if specified via CLI
|
|
171
|
+
if options[:data_dir]
|
|
172
|
+
KairosMcp.data_dir = File.expand_path(options[:data_dir])
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Handle --version
|
|
176
|
+
if options[:version]
|
|
177
|
+
puts "KairosChain MCP Server v#{KairosMcp::VERSION}"
|
|
178
|
+
puts "Data directory: #{KairosMcp.data_dir}"
|
|
179
|
+
exit
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Handle --init-admin
|
|
183
|
+
if options[:init_admin]
|
|
184
|
+
require 'kairos_mcp/auth/token_store'
|
|
185
|
+
|
|
186
|
+
store = KairosMcp::Auth::TokenStore.new(options[:token_store])
|
|
187
|
+
|
|
188
|
+
if !store.empty?
|
|
189
|
+
$stderr.puts "[WARNING] Active tokens already exist."
|
|
190
|
+
$stderr.puts "Use the token_manage MCP tool to create additional tokens."
|
|
191
|
+
$stderr.puts ""
|
|
192
|
+
$stderr.puts "Existing tokens:"
|
|
193
|
+
store.list.each do |t|
|
|
194
|
+
$stderr.puts " - #{t[:user]} (#{t[:role]}, expires: #{t[:expires_at] || 'never'})"
|
|
195
|
+
end
|
|
196
|
+
$stderr.puts ""
|
|
197
|
+
$stderr.puts "Proceed anyway? (y/N)"
|
|
198
|
+
answer = $stdin.gets&.strip
|
|
199
|
+
exit unless answer&.downcase == 'y'
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
result = store.create(user: 'admin', role: 'owner', issued_by: 'system')
|
|
203
|
+
|
|
204
|
+
puts ""
|
|
205
|
+
puts "=" * 60
|
|
206
|
+
puts " KairosChain Admin Token Generated"
|
|
207
|
+
puts "=" * 60
|
|
208
|
+
puts ""
|
|
209
|
+
puts " Token: #{result['raw_token']}"
|
|
210
|
+
puts " User: #{result['user']}"
|
|
211
|
+
puts " Role: #{result['role']}"
|
|
212
|
+
puts " Expires: #{result['expires_at'] || 'never'}"
|
|
213
|
+
puts ""
|
|
214
|
+
puts " IMPORTANT: Store this token securely."
|
|
215
|
+
puts " It will NOT be shown again."
|
|
216
|
+
puts ""
|
|
217
|
+
puts " Configure in Cursor mcp.json:"
|
|
218
|
+
puts " {"
|
|
219
|
+
puts " \"mcpServers\": {"
|
|
220
|
+
puts " \"kairos\": {"
|
|
221
|
+
puts " \"url\": \"http://localhost:#{options[:port] || 8080}/mcp\","
|
|
222
|
+
puts " \"headers\": {"
|
|
223
|
+
puts " \"Authorization\": \"Bearer #{result['raw_token']}\""
|
|
224
|
+
puts " }"
|
|
225
|
+
puts " }"
|
|
226
|
+
puts " }"
|
|
227
|
+
puts " }"
|
|
228
|
+
puts ""
|
|
229
|
+
puts "=" * 60
|
|
230
|
+
exit
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Auto-initialize if data directory doesn't exist
|
|
234
|
+
unless KairosMcp.initialized?
|
|
235
|
+
$stderr.puts "[KairosChain] Data directory not initialized at: #{KairosMcp.data_dir}"
|
|
236
|
+
$stderr.puts "[KairosChain] Run 'kairos-chain init' to set up, or auto-initializing..."
|
|
237
|
+
require 'kairos_mcp/initializer'
|
|
238
|
+
KairosMcp::Initializer.run(quiet: true)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Handle --http (Streamable HTTP mode)
|
|
242
|
+
if options[:http]
|
|
243
|
+
require 'kairos_mcp/http_server'
|
|
244
|
+
|
|
245
|
+
server = KairosMcp::HttpServer.new(
|
|
246
|
+
port: options[:port],
|
|
247
|
+
host: options[:host],
|
|
248
|
+
token_store_path: options[:token_store]
|
|
249
|
+
)
|
|
250
|
+
server.run
|
|
251
|
+
else
|
|
252
|
+
# Default: stdio mode
|
|
253
|
+
require 'kairos_mcp/server'
|
|
254
|
+
KairosMcp::Server.run
|
|
255
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Backward-compatible shim: kairos_mcp_server -> kairos-chain
|
|
5
|
+
#
|
|
6
|
+
# This executable is kept for backward compatibility with existing
|
|
7
|
+
# configurations (mcp.json, scripts, etc.). It delegates to the
|
|
8
|
+
# main kairos-chain executable.
|
|
9
|
+
|
|
10
|
+
$stderr.puts "[KairosChain] NOTE: 'kairos_mcp_server' is deprecated. Use 'kairos-chain' instead."
|
|
11
|
+
|
|
12
|
+
load File.expand_path('kairos-chain', __dir__)
|
data/lib/kairos-chain.rb
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'time'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
|
|
7
|
+
module KairosMcp
|
|
8
|
+
# ActionLog: Records actions for audit trail
|
|
9
|
+
#
|
|
10
|
+
# This class has been refactored to use the Storage backend abstraction.
|
|
11
|
+
# By default, it uses FileBackend (file-based storage).
|
|
12
|
+
# When SQLite is enabled, it uses SqliteBackend.
|
|
13
|
+
#
|
|
14
|
+
class ActionLog
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
# Record an action to the log
|
|
18
|
+
#
|
|
19
|
+
# @param action [String] The action performed
|
|
20
|
+
# @param skill_id [String, nil] The skill ID involved
|
|
21
|
+
# @param details [Hash, nil] Additional details
|
|
22
|
+
# @return [Boolean] Success status
|
|
23
|
+
def record(action:, skill_id: nil, details: nil)
|
|
24
|
+
entry = {
|
|
25
|
+
timestamp: Time.now.iso8601,
|
|
26
|
+
action: action,
|
|
27
|
+
skill_id: skill_id,
|
|
28
|
+
details: details
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
storage_backend.record_action(entry)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Get action history
|
|
35
|
+
#
|
|
36
|
+
# @param limit [Integer] Maximum number of entries to return
|
|
37
|
+
# @return [Array<Hash>] Recent action log entries
|
|
38
|
+
def history(limit: 50)
|
|
39
|
+
storage_backend.action_history(limit: limit)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Clear all action logs
|
|
43
|
+
#
|
|
44
|
+
# @return [Boolean] Success status
|
|
45
|
+
def clear!
|
|
46
|
+
storage_backend.clear_action_log!
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Get the storage backend type
|
|
50
|
+
# @return [Symbol] :file or :sqlite
|
|
51
|
+
def storage_type
|
|
52
|
+
storage_backend.backend_type
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Reset the storage backend (useful for testing)
|
|
56
|
+
def reset_backend!
|
|
57
|
+
@storage_backend = nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Set a custom storage backend (useful for testing or dependency injection)
|
|
61
|
+
# @param backend [Storage::Backend] The backend to use
|
|
62
|
+
def storage_backend=(backend)
|
|
63
|
+
@storage_backend = backend
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def storage_backend
|
|
69
|
+
@storage_backend ||= default_storage_backend
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def default_storage_backend
|
|
73
|
+
require_relative 'storage/backend'
|
|
74
|
+
Storage::Backend.default
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'erb'
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
require 'digest'
|
|
6
|
+
|
|
7
|
+
module KairosMcp
|
|
8
|
+
module Admin
|
|
9
|
+
# Helpers: ERB template helpers for the admin UI
|
|
10
|
+
#
|
|
11
|
+
# Provides HTML escaping, template rendering, flash messages,
|
|
12
|
+
# CSRF protection, and session management utilities.
|
|
13
|
+
#
|
|
14
|
+
module Helpers
|
|
15
|
+
VIEWS_DIR = File.expand_path('views', __dir__)
|
|
16
|
+
STATIC_DIR = File.expand_path('static', __dir__)
|
|
17
|
+
|
|
18
|
+
# HTML-escape a string to prevent XSS
|
|
19
|
+
#
|
|
20
|
+
# @param text [String, nil] Raw text
|
|
21
|
+
# @return [String] HTML-escaped text
|
|
22
|
+
def h(text)
|
|
23
|
+
ERB::Util.html_escape(text.to_s)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Render an ERB template
|
|
27
|
+
#
|
|
28
|
+
# @param template_name [String] Template file name (without .erb)
|
|
29
|
+
# @param layout [Boolean] Whether to wrap in layout
|
|
30
|
+
# @param locals [Hash] Local variables for the template
|
|
31
|
+
# @return [String] Rendered HTML
|
|
32
|
+
def render(template_name, layout: true, **locals)
|
|
33
|
+
template_path = File.join(VIEWS_DIR, "#{template_name}.erb")
|
|
34
|
+
template = ERB.new(File.read(template_path), trim_mode: '-')
|
|
35
|
+
|
|
36
|
+
# Make locals available as instance variables for ERB binding
|
|
37
|
+
b = binding
|
|
38
|
+
locals.each { |k, v| b.local_variable_set(k, v) }
|
|
39
|
+
|
|
40
|
+
content = template.result(b)
|
|
41
|
+
|
|
42
|
+
if layout
|
|
43
|
+
render_layout(content)
|
|
44
|
+
else
|
|
45
|
+
content
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Render a partial (no layout)
|
|
50
|
+
#
|
|
51
|
+
# @param partial_name [String] Partial file name (without .erb, with _prefix)
|
|
52
|
+
# @param locals [Hash] Local variables
|
|
53
|
+
# @return [String] Rendered HTML fragment
|
|
54
|
+
def render_partial(partial_name, **locals)
|
|
55
|
+
render("partials/#{partial_name}", layout: false, **locals)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Wrap content in the shared layout
|
|
59
|
+
#
|
|
60
|
+
# @param content [String] Page content HTML
|
|
61
|
+
# @return [String] Full HTML page
|
|
62
|
+
def render_layout(content)
|
|
63
|
+
@content = content
|
|
64
|
+
layout_path = File.join(VIEWS_DIR, 'layout.erb')
|
|
65
|
+
layout_template = ERB.new(File.read(layout_path), trim_mode: '-')
|
|
66
|
+
layout_template.result(binding)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Generate an HTML response
|
|
70
|
+
#
|
|
71
|
+
# @param status [Integer] HTTP status code
|
|
72
|
+
# @param body [String] HTML body
|
|
73
|
+
# @return [Array] Rack response triple
|
|
74
|
+
def html_response(status, body)
|
|
75
|
+
[status, { 'Content-Type' => 'text/html; charset=utf-8' }, [body]]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Redirect response
|
|
79
|
+
#
|
|
80
|
+
# @param path [String] Redirect target
|
|
81
|
+
# @return [Array] Rack response triple
|
|
82
|
+
def redirect(path)
|
|
83
|
+
[302, { 'Location' => path }, []]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Serve a static file
|
|
87
|
+
#
|
|
88
|
+
# @param filename [String] File name in static/ directory
|
|
89
|
+
# @return [Array] Rack response triple
|
|
90
|
+
def serve_static(filename)
|
|
91
|
+
filepath = File.join(STATIC_DIR, filename)
|
|
92
|
+
return [404, {}, ['Not found']] unless File.exist?(filepath)
|
|
93
|
+
|
|
94
|
+
content_type = case File.extname(filename)
|
|
95
|
+
when '.css' then 'text/css'
|
|
96
|
+
when '.js' then 'application/javascript'
|
|
97
|
+
when '.png' then 'image/png'
|
|
98
|
+
when '.svg' then 'image/svg+xml'
|
|
99
|
+
else 'application/octet-stream'
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
[200, { 'Content-Type' => content_type, 'Cache-Control' => 'public, max-age=3600' },
|
|
103
|
+
[File.read(filepath)]]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# -----------------------------------------------------------------------
|
|
107
|
+
# Session Management
|
|
108
|
+
# -----------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
SESSION_COOKIE = 'kairos_admin_session'
|
|
111
|
+
SESSION_SECRET = ENV['KAIROS_SESSION_SECRET'] || SecureRandom.hex(32)
|
|
112
|
+
|
|
113
|
+
# Encode a session value into a signed cookie
|
|
114
|
+
#
|
|
115
|
+
# @param data [Hash] Session data
|
|
116
|
+
# @return [String] Signed cookie value
|
|
117
|
+
def encode_session(data)
|
|
118
|
+
payload = JSON.generate(data)
|
|
119
|
+
encoded = [payload].pack('m0') # Base64 (no newlines)
|
|
120
|
+
signature = sign(encoded)
|
|
121
|
+
"#{encoded}--#{signature}"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Decode and verify a signed session cookie
|
|
125
|
+
#
|
|
126
|
+
# @param cookie_value [String] Raw cookie value
|
|
127
|
+
# @return [Hash, nil] Session data or nil if invalid
|
|
128
|
+
def decode_session(cookie_value)
|
|
129
|
+
return nil unless cookie_value
|
|
130
|
+
|
|
131
|
+
encoded, signature = cookie_value.split('--', 2)
|
|
132
|
+
return nil unless encoded && signature
|
|
133
|
+
return nil unless secure_compare(sign(encoded), signature)
|
|
134
|
+
|
|
135
|
+
payload = encoded.unpack1('m0')
|
|
136
|
+
JSON.parse(payload, symbolize_names: true)
|
|
137
|
+
rescue StandardError
|
|
138
|
+
nil
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Extract session from Rack env
|
|
142
|
+
#
|
|
143
|
+
# @param env [Hash] Rack environment
|
|
144
|
+
# @return [Hash, nil] Session data
|
|
145
|
+
def get_session(env)
|
|
146
|
+
cookies = parse_cookies(env)
|
|
147
|
+
cookie_value = cookies[SESSION_COOKIE]
|
|
148
|
+
decode_session(cookie_value)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Build Set-Cookie header for session
|
|
152
|
+
#
|
|
153
|
+
# @param data [Hash] Session data
|
|
154
|
+
# @return [String] Set-Cookie header value
|
|
155
|
+
def session_cookie(data)
|
|
156
|
+
value = encode_session(data)
|
|
157
|
+
"#{SESSION_COOKIE}=#{value}; Path=/admin; HttpOnly; SameSite=Strict"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Build Set-Cookie header to clear session
|
|
161
|
+
#
|
|
162
|
+
# @return [String] Set-Cookie header value
|
|
163
|
+
def clear_session_cookie
|
|
164
|
+
"#{SESSION_COOKIE}=; Path=/admin; HttpOnly; SameSite=Strict; Max-Age=0"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# -----------------------------------------------------------------------
|
|
168
|
+
# CSRF Protection
|
|
169
|
+
# -----------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
# Generate a CSRF token
|
|
172
|
+
#
|
|
173
|
+
# @return [String] CSRF token
|
|
174
|
+
def generate_csrf_token
|
|
175
|
+
SecureRandom.hex(32)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Verify a CSRF token from form submission
|
|
179
|
+
#
|
|
180
|
+
# @param env [Hash] Rack environment
|
|
181
|
+
# @param session [Hash] Current session
|
|
182
|
+
# @return [Boolean] Whether CSRF token is valid
|
|
183
|
+
def valid_csrf?(env, session)
|
|
184
|
+
body = env['rack.input']&.read
|
|
185
|
+
env['rack.input']&.rewind
|
|
186
|
+
return false unless body
|
|
187
|
+
|
|
188
|
+
params = parse_form_body(body)
|
|
189
|
+
submitted_token = params['_csrf']
|
|
190
|
+
session_token = session&.dig(:csrf_token)
|
|
191
|
+
|
|
192
|
+
return false unless submitted_token && session_token
|
|
193
|
+
|
|
194
|
+
secure_compare(submitted_token, session_token)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# -----------------------------------------------------------------------
|
|
198
|
+
# Form/Cookie Parsing
|
|
199
|
+
# -----------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
# Parse cookies from Rack env
|
|
202
|
+
#
|
|
203
|
+
# @param env [Hash] Rack environment
|
|
204
|
+
# @return [Hash] Cookie name → value
|
|
205
|
+
def parse_cookies(env)
|
|
206
|
+
cookie_header = env['HTTP_COOKIE'] || ''
|
|
207
|
+
cookie_header.split(';').each_with_object({}) do |pair, hash|
|
|
208
|
+
key, value = pair.strip.split('=', 2)
|
|
209
|
+
hash[key] = value if key
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Parse URL-encoded form body
|
|
214
|
+
#
|
|
215
|
+
# @param body [String] Form body
|
|
216
|
+
# @return [Hash] Parameter name → value
|
|
217
|
+
def parse_form_body(body)
|
|
218
|
+
body.split('&').each_with_object({}) do |pair, hash|
|
|
219
|
+
key, value = pair.split('=', 2)
|
|
220
|
+
hash[URI.decode_www_form_component(key)] = URI.decode_www_form_component(value || '')
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Parse query string
|
|
225
|
+
#
|
|
226
|
+
# @param env [Hash] Rack environment
|
|
227
|
+
# @return [Hash] Query parameters
|
|
228
|
+
def parse_query(env)
|
|
229
|
+
qs = env['QUERY_STRING'] || ''
|
|
230
|
+
parse_form_body(qs)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
private
|
|
234
|
+
|
|
235
|
+
def sign(data)
|
|
236
|
+
Digest::SHA256.hexdigest("#{data}#{SESSION_SECRET}")
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def secure_compare(a, b)
|
|
240
|
+
return false unless a.bytesize == b.bytesize
|
|
241
|
+
|
|
242
|
+
l = a.unpack("C#{a.bytesize}")
|
|
243
|
+
r = b.unpack("C#{b.bytesize}")
|
|
244
|
+
result = 0
|
|
245
|
+
l.zip(r) { |x, y| result |= x ^ y }
|
|
246
|
+
result.zero?
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|