kairos-chain 3.9.4 → 3.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 +23 -0
- data/lib/kairos_mcp/safety.rb +6 -0
- data/lib/kairos_mcp/tools/skills_promote.rb +30 -4
- data/lib/kairos_mcp/tools/system_upgrade.rb +32 -0
- data/lib/kairos_mcp/version.rb +1 -1
- data/templates/skillsets/agent/tools/agent_start.rb +3 -1
- data/templates/skillsets/dream/lib/dream/scanner.rb +102 -7
- data/templates/skillsets/dream/tools/dream_scan.rb +9 -2
- data/templates/skillsets/introspection/config/introspection.yml +4 -0
- data/templates/skillsets/introspection/knowledge/introspection_guide/introspection_guide.md +90 -0
- data/templates/skillsets/introspection/lib/introspection/health_scorer.rb +136 -0
- data/templates/skillsets/introspection/lib/introspection/safety_inspector.rb +85 -0
- data/templates/skillsets/introspection/lib/introspection.rb +7 -0
- data/templates/skillsets/introspection/skillset.json +17 -0
- data/templates/skillsets/introspection/tools/introspection_check.rb +207 -0
- data/templates/skillsets/introspection/tools/introspection_health.rb +80 -0
- data/templates/skillsets/introspection/tools/introspection_safety.rb +46 -0
- metadata +12 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 13fdfa15efd40f228b2fb789d51e1cbd722ce732bde40df4c8d25e5bf4108c56
|
|
4
|
+
data.tar.gz: 3aa43d1c26a56af6aa5c24e7ea9ca1d26a20f5c95984aec77f574874beeb810e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 80630f2c2488d3ab1b3864abc33f3f3711507c7ad1d6d166c21b06f33d8c044f51ec518ef22ae1bacc98e6c1a8d06dcff80afccb0d9b6b2b432dd38d7ad220ee
|
|
7
|
+
data.tar.gz: 27d701df365ec0c71b3b16b9190c78d0d590964192316d0d4a08218074ff289b5ca7edbd6fcca41311f9f0daeaed4b0a5ad46091bb36767afb1b2839e5dfab99
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@ 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
|
+
## [3.10.0] - 2026-03-31
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **introspection SkillSet** (v0.1.0) — New self-inspection SkillSet with 3 tools:
|
|
12
|
+
- `introspection_check`: Full inspection (L1 health + blockchain integrity + safety mechanisms + recommendations)
|
|
13
|
+
- `introspection_health`: L1 knowledge health scores using Synoptis TrustScorer (optional, falls back to staleness-only)
|
|
14
|
+
- `introspection_safety`: 4-layer safety mechanism visibility (L0 approval workflow, RBAC policies, agent safety gates, blockchain recording)
|
|
15
|
+
- **Dream SkillSet L1 dedup + confidence scoring** (v0.2.1) — `dream_scan` now checks promotion candidates against existing L1 knowledge (name similarity + tag Jaccard overlap) and scores candidates with 3-dimension confidence (recurrence, tag consistency, session diversity). New `include_l1_dedup` parameter.
|
|
16
|
+
- **`skills_promote` attestation integration** — Successful L2→L1 promotions now automatically issue Synoptis attestations (`claim: "promoted_from_l2"`, `actor_role: "automated"`). Graceful degradation when Synoptis is not loaded.
|
|
17
|
+
- **`Safety.registered_policy_names`** — New thread-safe public API for introspecting registered RBAC policies. Replaces `instance_variable_get(:@policies)` pattern.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **`system_upgrade` SkillSet-only install** — When specific SkillSet names are provided via `names` parameter but L0 templates are already up-to-date, `system_upgrade apply` now correctly installs/upgrades the requested SkillSets instead of returning "No upgrade needed". Previously, the L0 upgrade check (`UpgradeAnalyzer.upgrade_needed?`) gated all operations including SkillSet installs.
|
|
22
|
+
|
|
23
|
+
### Design Process
|
|
24
|
+
|
|
25
|
+
- Design reviewed: 2 rounds x 3 LLMs (Claude Opus 4.6, Codex GPT-5.4, Cursor Composer-2)
|
|
26
|
+
- Implementation reviewed: 1 round x 3 LLMs per phase
|
|
27
|
+
- 8 P0/P1 bugs found and fixed during design review (before any code was written)
|
|
28
|
+
- Inspired by oh-my-claudecode analysis; independently designed with KairosChain philosophy
|
|
29
|
+
|
|
7
30
|
## [3.9.4] - 2026-03-30
|
|
8
31
|
|
|
9
32
|
### Added
|
data/lib/kairos_mcp/safety.rb
CHANGED
|
@@ -25,6 +25,12 @@ module KairosMcp
|
|
|
25
25
|
@policy_mutex.synchronize { @policies[name.to_sym] }
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
# Thread-safe list of registered policy names.
|
|
29
|
+
# Used by introspection SkillSet for safety visibility.
|
|
30
|
+
def self.registered_policy_names
|
|
31
|
+
@policy_mutex.synchronize { @policies.keys.map(&:to_s) }
|
|
32
|
+
end
|
|
33
|
+
|
|
28
34
|
# For testing only
|
|
29
35
|
def self.clear_policies!
|
|
30
36
|
@policy_mutex.synchronize { @policies = {} }
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'json'
|
|
3
4
|
require_relative 'base_tool'
|
|
4
5
|
require_relative '../knowledge_provider'
|
|
5
6
|
require_relative '../context_manager'
|
|
@@ -30,7 +31,8 @@ module KairosMcp
|
|
|
30
31
|
|
|
31
32
|
def description
|
|
32
33
|
'Promote knowledge between layers (L2→L1, L1→L0) with optional Persona Assembly for decision support. ' \
|
|
33
|
-
'Assembly generates a structured discussion from multiple perspectives before human decision.'
|
|
34
|
+
'Assembly generates a structured discussion from multiple perspectives before human decision. ' \
|
|
35
|
+
'For pattern detection and auto-scan, use dream_scan instead.'
|
|
34
36
|
end
|
|
35
37
|
|
|
36
38
|
def category
|
|
@@ -63,7 +65,7 @@ module KairosMcp
|
|
|
63
65
|
end
|
|
64
66
|
|
|
65
67
|
def related_tools
|
|
66
|
-
%w[context_save knowledge_update skills_evolve skills_audit]
|
|
68
|
+
%w[context_save knowledge_update skills_evolve skills_audit attestation_issue]
|
|
67
69
|
end
|
|
68
70
|
|
|
69
71
|
def input_schema
|
|
@@ -72,7 +74,7 @@ module KairosMcp
|
|
|
72
74
|
properties: {
|
|
73
75
|
command: {
|
|
74
76
|
type: 'string',
|
|
75
|
-
description: 'Command: "analyze" (with assembly), "promote" (direct promotion), "status" (check requirements), or "suggest" (LLM suggests optimal personas
|
|
77
|
+
description: 'Command: "analyze" (with assembly), "promote" (direct promotion), "status" (check requirements), or "suggest" (LLM suggests optimal personas)',
|
|
76
78
|
enum: %w[analyze promote status suggest]
|
|
77
79
|
},
|
|
78
80
|
source_name: {
|
|
@@ -122,7 +124,7 @@ module KairosMcp
|
|
|
122
124
|
consensus_threshold: {
|
|
123
125
|
type: 'number',
|
|
124
126
|
description: 'Consensus threshold for early termination in discussion mode (default: 0.6 = 60%)'
|
|
125
|
-
}
|
|
127
|
+
},
|
|
126
128
|
},
|
|
127
129
|
required: %w[command]
|
|
128
130
|
}
|
|
@@ -576,6 +578,9 @@ module KairosMcp
|
|
|
576
578
|
# Track promotion event for state commit (this triggers auto-commit on promotion)
|
|
577
579
|
track_promotion_change(from_layer: 'L2', to_layer: 'L1', skill_id: target_name, reason: reason)
|
|
578
580
|
|
|
581
|
+
# Issue attestation AFTER L1 exists
|
|
582
|
+
issue_promotion_attestation(target_name, reason)
|
|
583
|
+
|
|
579
584
|
action = existing ? 'updated' : 'created'
|
|
580
585
|
output = "## Promotion Successful\n\n"
|
|
581
586
|
output += "**Target**: #{target_name} (L1)\n"
|
|
@@ -681,6 +686,27 @@ module KairosMcp
|
|
|
681
686
|
end
|
|
682
687
|
end
|
|
683
688
|
|
|
689
|
+
# Issue attestation after successful promotion
|
|
690
|
+
def issue_promotion_attestation(target_name, reason)
|
|
691
|
+
return unless synoptis_available?
|
|
692
|
+
|
|
693
|
+
invoke_tool('attestation_issue', {
|
|
694
|
+
'subject_ref' => "knowledge://#{target_name}",
|
|
695
|
+
'claim' => 'promoted_from_l2',
|
|
696
|
+
'evidence' => reason.to_s,
|
|
697
|
+
'actor_role' => 'automated'
|
|
698
|
+
})
|
|
699
|
+
rescue StandardError => e
|
|
700
|
+
# Synoptis may not be loaded — silently skip
|
|
701
|
+
warn "[SkillsPromote] Attestation skipped: #{e.message}"
|
|
702
|
+
end
|
|
703
|
+
|
|
704
|
+
def synoptis_available?
|
|
705
|
+
@registry && true
|
|
706
|
+
rescue StandardError
|
|
707
|
+
false
|
|
708
|
+
end
|
|
709
|
+
|
|
684
710
|
# Track promotion event for state commit auto-commit
|
|
685
711
|
def track_promotion_change(from_layer:, to_layer:, skill_id:, reason: nil)
|
|
686
712
|
return unless SkillsConfig.state_commit_enabled?
|
|
@@ -240,6 +240,12 @@ module KairosMcp
|
|
|
240
240
|
analyzer = UpgradeAnalyzer.new
|
|
241
241
|
analyzer.analyze
|
|
242
242
|
|
|
243
|
+
# When specific SkillSet names are requested, skip L0 upgrade check
|
|
244
|
+
# and go directly to SkillSet install/upgrade
|
|
245
|
+
if names && !names.empty? && !analyzer.upgrade_needed?
|
|
246
|
+
return handle_skillset_only_install(names)
|
|
247
|
+
end
|
|
248
|
+
|
|
243
249
|
unless analyzer.upgrade_needed?
|
|
244
250
|
return text_content("No upgrade needed. Data directory is already up to date.")
|
|
245
251
|
end
|
|
@@ -437,6 +443,32 @@ module KairosMcp
|
|
|
437
443
|
# =====================================================================
|
|
438
444
|
# skillsets — List all available SkillSets with install status
|
|
439
445
|
# =====================================================================
|
|
446
|
+
# Install/upgrade specific SkillSets without requiring a full L0 upgrade.
|
|
447
|
+
# Called when names are specified but L0 templates are already up to date.
|
|
448
|
+
def handle_skillset_only_install(names)
|
|
449
|
+
ss_mgr = KairosMcp::SkillSetManager.new
|
|
450
|
+
ss_results = ss_mgr.upgrade_apply(names: names)
|
|
451
|
+
|
|
452
|
+
if ss_results.empty?
|
|
453
|
+
return text_content("No SkillSet changes needed for: #{names.join(', ')}")
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
output = "# SkillSet Install/Upgrade\n\n"
|
|
457
|
+
ss_results.each do |r|
|
|
458
|
+
if r[:action] == 'installed'
|
|
459
|
+
output += " [INSTALLED] #{r[:name]} v#{r[:to]}\n"
|
|
460
|
+
else
|
|
461
|
+
output += " [UPGRADED] #{r[:name]} v#{r[:from]} → v#{r[:to]} (#{r[:files_updated]} files)\n"
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
output += "\nNo L0 template changes needed.\n"
|
|
465
|
+
output += "Restart the MCP server to load new SkillSet tools.\n"
|
|
466
|
+
|
|
467
|
+
text_content(output)
|
|
468
|
+
rescue StandardError => e
|
|
469
|
+
text_content("SkillSet install failed: #{e.message}")
|
|
470
|
+
end
|
|
471
|
+
|
|
440
472
|
def handle_skillsets
|
|
441
473
|
ss_mgr = KairosMcp::SkillSetManager.new
|
|
442
474
|
available = ss_mgr.available_skillsets
|
data/lib/kairos_mcp/version.rb
CHANGED
|
@@ -39,7 +39,9 @@ module KairosMcp
|
|
|
39
39
|
properties: {
|
|
40
40
|
goal_name: {
|
|
41
41
|
type: 'string',
|
|
42
|
-
description: 'Name of the goal
|
|
42
|
+
description: 'Name of the goal. Create it with context_save (L2) first. ' \
|
|
43
|
+
'L1 knowledge is only for reusable goal templates. ' \
|
|
44
|
+
'The agent searches L2 contexts first, then falls back to L1.'
|
|
43
45
|
},
|
|
44
46
|
max_cycles: {
|
|
45
47
|
type: 'integer',
|
|
@@ -25,8 +25,9 @@ module KairosMcp
|
|
|
25
25
|
# @param scope [String] 'l2', 'l1', or 'all'
|
|
26
26
|
# @param since_session [String, nil] Only scan sessions after this ID
|
|
27
27
|
# @param include_archive_candidates [Boolean] Whether to detect stale L2
|
|
28
|
+
# @param include_l1_dedup [Boolean] Check promotion candidates against existing L1
|
|
28
29
|
# @return [Hash] Structured scan result
|
|
29
|
-
def scan(scope: 'l2', since_session: nil, include_archive_candidates: true)
|
|
30
|
+
def scan(scope: 'l2', since_session: nil, include_archive_candidates: true, include_l1_dedup: true)
|
|
30
31
|
result = {
|
|
31
32
|
scope: scope,
|
|
32
33
|
scanned_at: Time.now.iso8601,
|
|
@@ -38,7 +39,8 @@ module KairosMcp
|
|
|
38
39
|
|
|
39
40
|
if %w[l2 all].include?(scope)
|
|
40
41
|
scan_l2(result, since_session: since_session,
|
|
41
|
-
include_archive_candidates: include_archive_candidates
|
|
42
|
+
include_archive_candidates: include_archive_candidates,
|
|
43
|
+
include_l1_dedup: include_l1_dedup)
|
|
42
44
|
end
|
|
43
45
|
|
|
44
46
|
if %w[l1 all].include?(scope)
|
|
@@ -54,7 +56,7 @@ module KairosMcp
|
|
|
54
56
|
# L2 scanning
|
|
55
57
|
# ---------------------------------------------------------------
|
|
56
58
|
|
|
57
|
-
def scan_l2(result, since_session: nil, include_archive_candidates: true)
|
|
59
|
+
def scan_l2(result, since_session: nil, include_archive_candidates: true, include_l1_dedup: true)
|
|
58
60
|
all_contexts = load_all_l2_contexts(since_session: since_session)
|
|
59
61
|
|
|
60
62
|
# Partition: live contexts vs archived stubs
|
|
@@ -62,7 +64,20 @@ module KairosMcp
|
|
|
62
64
|
|
|
63
65
|
# Promotion candidates: tag co-occurrence across sessions
|
|
64
66
|
sessions_tags = build_sessions_tags(live)
|
|
65
|
-
|
|
67
|
+
candidates = detect_promotion_candidates(sessions_tags, all_sessions_tags: sessions_tags)
|
|
68
|
+
|
|
69
|
+
# L1 dedup: mark candidates that already exist as L1 knowledge
|
|
70
|
+
if include_l1_dedup
|
|
71
|
+
kp = knowledge_provider
|
|
72
|
+
existing_l1 = kp ? kp.list : []
|
|
73
|
+
candidates.each do |candidate|
|
|
74
|
+
match = find_l1_match(candidate[:tag], Array(candidate[:tag]), existing_l1)
|
|
75
|
+
candidate[:already_in_l1] = !match.nil?
|
|
76
|
+
candidate[:l1_match] = match if match
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
result[:promotion_candidates] = candidates
|
|
66
81
|
|
|
67
82
|
# Consolidation candidates: name token overlap
|
|
68
83
|
result[:consolidation_candidates] = detect_consolidation_candidates(live)
|
|
@@ -132,8 +147,11 @@ module KairosMcp
|
|
|
132
147
|
# Detect tags that recur across min_recurrence+ distinct sessions.
|
|
133
148
|
#
|
|
134
149
|
# @param sessions_tags [Hash] { session_id => { context_name => [tags] } }
|
|
135
|
-
# @
|
|
136
|
-
|
|
150
|
+
# @param all_sessions_tags [Hash] same as sessions_tags (for scoring)
|
|
151
|
+
# @return [Array<Hash>] promotion candidate hashes with confidence scoring
|
|
152
|
+
def detect_promotion_candidates(sessions_tags, all_sessions_tags: nil)
|
|
153
|
+
all_sessions_tags ||= sessions_tags
|
|
154
|
+
|
|
137
155
|
tag_sessions = Hash.new { |h, k| h[k] = Set.new }
|
|
138
156
|
|
|
139
157
|
sessions_tags.each do |session_id, contexts|
|
|
@@ -148,12 +166,15 @@ module KairosMcp
|
|
|
148
166
|
.sort_by { |_tag, sids| -sids.size }
|
|
149
167
|
.first(@max_candidates)
|
|
150
168
|
.map do |tag, sids|
|
|
169
|
+
confidence = score_promotion_candidate(tag, sids, all_sessions_tags)
|
|
151
170
|
{
|
|
152
171
|
tag: tag,
|
|
153
172
|
session_count: sids.size,
|
|
154
173
|
sessions: sids.to_a,
|
|
155
174
|
signal: 'tag_recurrence',
|
|
156
|
-
strength: sids.size.to_f / @min_recurrence
|
|
175
|
+
strength: sids.size.to_f / @min_recurrence,
|
|
176
|
+
confidence: confidence,
|
|
177
|
+
already_in_l1: false
|
|
157
178
|
}
|
|
158
179
|
end
|
|
159
180
|
|
|
@@ -274,6 +295,80 @@ module KairosMcp
|
|
|
274
295
|
tags
|
|
275
296
|
end
|
|
276
297
|
|
|
298
|
+
# ---------------------------------------------------------------
|
|
299
|
+
# L1 dedup and confidence scoring (migrated from skills_promote auto_scan)
|
|
300
|
+
# ---------------------------------------------------------------
|
|
301
|
+
|
|
302
|
+
# Find an existing L1 entry that matches a candidate by name or tag overlap.
|
|
303
|
+
#
|
|
304
|
+
# @param candidate_name [String] suggested/tag name of the candidate
|
|
305
|
+
# @param candidate_tags [Array<String>] tags for the candidate
|
|
306
|
+
# @param existing_l1 [Array<Hash>] list from KnowledgeProvider#list
|
|
307
|
+
# @return [String, nil] matching L1 entry name or nil
|
|
308
|
+
def find_l1_match(candidate_name, candidate_tags, existing_l1)
|
|
309
|
+
existing_l1.each do |entry|
|
|
310
|
+
# Name match (normalized substring ratio)
|
|
311
|
+
name_sim = name_similarity(candidate_name, entry[:name])
|
|
312
|
+
return entry[:name] if name_sim > 0.8
|
|
313
|
+
|
|
314
|
+
# Tag overlap (Jaccard on name tokens vs entry tags)
|
|
315
|
+
entry_tags = Array(entry[:tags]).to_set
|
|
316
|
+
candidate_set = candidate_tags.to_set
|
|
317
|
+
sim = candidate_set.empty? && entry_tags.empty? ? 0.0 : (candidate_set & entry_tags).size.to_f / (candidate_set | entry_tags).size.to_f
|
|
318
|
+
return entry[:name] if sim > 0.8
|
|
319
|
+
end
|
|
320
|
+
nil
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Compute name similarity using token-level Jaccard.
|
|
324
|
+
#
|
|
325
|
+
# @param a [String]
|
|
326
|
+
# @param b [String]
|
|
327
|
+
# @return [Float] 0.0..1.0
|
|
328
|
+
def name_similarity(a, b)
|
|
329
|
+
a_parts = a.to_s.split('_').to_set
|
|
330
|
+
b_parts = b.to_s.split('_').to_set
|
|
331
|
+
return 0.0 if a_parts.empty? && b_parts.empty?
|
|
332
|
+
(a_parts & b_parts).size.to_f / [a_parts.size, b_parts.size].max
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Score a promotion candidate across 3 dimensions:
|
|
336
|
+
# - recurrence: how many sessions the tag appears in (max 40)
|
|
337
|
+
# - tag_consistency: co-occurrence consistency across sessions (max 30)
|
|
338
|
+
# - session_diversity: distinct sessions (max 30)
|
|
339
|
+
#
|
|
340
|
+
# @param tag [String] the recurring tag
|
|
341
|
+
# @param sessions [Set<String>] session IDs where this tag appears
|
|
342
|
+
# @param all_sessions_tags [Hash] { session_id => { context_name => [tags] } }
|
|
343
|
+
# @return [Integer] 0..100
|
|
344
|
+
def score_promotion_candidate(tag, sessions, all_sessions_tags)
|
|
345
|
+
# Recurrence: 3=10, 4=20, 5=30, 6+=40 (max 40 points)
|
|
346
|
+
recurrence_score = [[sessions.size - 2, 4].min * 10, 40].min
|
|
347
|
+
recurrence_score = [recurrence_score, 0].max
|
|
348
|
+
|
|
349
|
+
# Tag consistency: what fraction of sessions containing this tag
|
|
350
|
+
# also share the same co-occurring tags?
|
|
351
|
+
co_tags_per_session = sessions.map do |sid|
|
|
352
|
+
contexts = all_sessions_tags[sid] || {}
|
|
353
|
+
contexts.values.flatten.uniq.reject { |t| t == tag }
|
|
354
|
+
end.reject(&:empty?)
|
|
355
|
+
|
|
356
|
+
if co_tags_per_session.size >= 2
|
|
357
|
+
co_sets = co_tags_per_session.map(&:to_set)
|
|
358
|
+
intersection = co_sets.reduce(:&)
|
|
359
|
+
union = co_sets.reduce(:|)
|
|
360
|
+
tag_consistency = union.empty? ? 0.0 : intersection.size.to_f / union.size
|
|
361
|
+
else
|
|
362
|
+
tag_consistency = 0.0
|
|
363
|
+
end
|
|
364
|
+
tag_score = (tag_consistency * 30).round
|
|
365
|
+
|
|
366
|
+
# Session diversity: distinct sessions (max 30 points)
|
|
367
|
+
session_score = [[sessions.size - 1, 3].min * 10, 30].min
|
|
368
|
+
|
|
369
|
+
[recurrence_score + tag_score + session_score, 100].min
|
|
370
|
+
end
|
|
371
|
+
|
|
277
372
|
# ---------------------------------------------------------------
|
|
278
373
|
# Helpers
|
|
279
374
|
# ---------------------------------------------------------------
|
|
@@ -42,6 +42,10 @@ module KairosMcp
|
|
|
42
42
|
include_archive_candidates: {
|
|
43
43
|
type: 'boolean',
|
|
44
44
|
description: 'Whether to detect stale L2 contexts for archival. Default: true'
|
|
45
|
+
},
|
|
46
|
+
include_l1_dedup: {
|
|
47
|
+
type: 'boolean',
|
|
48
|
+
description: 'Check promotion candidates against existing L1 knowledge to mark duplicates. Default: true'
|
|
45
49
|
}
|
|
46
50
|
},
|
|
47
51
|
required: []
|
|
@@ -55,7 +59,8 @@ module KairosMcp
|
|
|
55
59
|
scan_result = scanner.scan(
|
|
56
60
|
scope: arguments['scope'] || config.dig('scan', 'default_scope') || 'l2',
|
|
57
61
|
since_session: arguments['since_session'],
|
|
58
|
-
include_archive_candidates: arguments.fetch('include_archive_candidates', true)
|
|
62
|
+
include_archive_candidates: arguments.fetch('include_archive_candidates', true),
|
|
63
|
+
include_l1_dedup: arguments.fetch('include_l1_dedup', true)
|
|
59
64
|
)
|
|
60
65
|
|
|
61
66
|
# Record findings on blockchain if non-empty
|
|
@@ -131,7 +136,9 @@ module KairosMcp
|
|
|
131
136
|
lines << "_No recurring patterns detected._"
|
|
132
137
|
else
|
|
133
138
|
promo.each do |c|
|
|
134
|
-
|
|
139
|
+
dedup_marker = c[:already_in_l1] ? " [already in L1: #{c[:l1_match]}]" : ''
|
|
140
|
+
confidence_str = c[:confidence] ? " (confidence: #{c[:confidence]})" : ''
|
|
141
|
+
lines << "- **#{c[:tag]}**: #{c[:session_count]} sessions (strength: #{c[:strength].round(2)})#{confidence_str}#{dedup_marker}"
|
|
135
142
|
end
|
|
136
143
|
end
|
|
137
144
|
lines << ""
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Introspection Guide
|
|
3
|
+
description: Usage guide for the introspection SkillSet — self-inspection, health scoring, and safety visibility
|
|
4
|
+
version: "0.1.0"
|
|
5
|
+
tags:
|
|
6
|
+
- introspection
|
|
7
|
+
- health
|
|
8
|
+
- safety
|
|
9
|
+
- blockchain
|
|
10
|
+
- maintenance
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Introspection Guide
|
|
14
|
+
|
|
15
|
+
## Overview
|
|
16
|
+
|
|
17
|
+
The introspection SkillSet provides self-inspection capabilities for KairosChain.
|
|
18
|
+
It examines knowledge health, blockchain integrity, and safety mechanisms to produce
|
|
19
|
+
actionable reports and recommendations.
|
|
20
|
+
|
|
21
|
+
## Tools
|
|
22
|
+
|
|
23
|
+
### introspection_check
|
|
24
|
+
|
|
25
|
+
Full self-inspection combining all domains. Returns a consolidated report with
|
|
26
|
+
recommendations.
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
introspection_check() # All domains, markdown format
|
|
30
|
+
introspection_check(format: "json") # JSON output
|
|
31
|
+
introspection_check(domains: ["health"]) # Health only
|
|
32
|
+
introspection_check(domains: ["blockchain"]) # Blockchain only
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### introspection_health
|
|
36
|
+
|
|
37
|
+
Focused L1 knowledge health scoring.
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
introspection_health() # All entries
|
|
41
|
+
introspection_health(name: "my_knowledge") # Single entry
|
|
42
|
+
introspection_health(below_threshold: 0.5) # Only unhealthy entries
|
|
43
|
+
introspection_health(sort_by: "name") # Sort alphabetically
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### introspection_safety
|
|
47
|
+
|
|
48
|
+
Safety mechanism visibility across all layers.
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
introspection_safety() # Returns 4-layer safety report
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Health Scoring
|
|
55
|
+
|
|
56
|
+
Health scores range from 0.0 (unhealthy) to 1.0 (healthy).
|
|
57
|
+
|
|
58
|
+
When Synoptis TrustScorer is available:
|
|
59
|
+
- **Trust score** (70% weight): Based on attestation count and quality
|
|
60
|
+
- **Staleness score** (30% weight): Based on file modification time
|
|
61
|
+
|
|
62
|
+
When TrustScorer is not available:
|
|
63
|
+
- **Staleness score only** (100%): Linear decay over configurable threshold
|
|
64
|
+
|
|
65
|
+
### Staleness Threshold
|
|
66
|
+
|
|
67
|
+
Default: 180 days. Configure in `config/introspection.yml`:
|
|
68
|
+
|
|
69
|
+
```yaml
|
|
70
|
+
introspection:
|
|
71
|
+
health:
|
|
72
|
+
staleness_days: 90 # More aggressive freshness requirement
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Safety Layers
|
|
76
|
+
|
|
77
|
+
The safety report covers four layers:
|
|
78
|
+
|
|
79
|
+
1. **L0 Approval Workflow**: Whether Kairos DSL approval_workflow skill is loaded
|
|
80
|
+
2. **Runtime RBAC**: Registered Safety policies (can_modify_l0, etc.)
|
|
81
|
+
3. **Agent Safety Gates**: Autonomous mode limits from agent.yml
|
|
82
|
+
4. **Blockchain Recording**: Chain integrity and block count
|
|
83
|
+
|
|
84
|
+
## Recommendations
|
|
85
|
+
|
|
86
|
+
The full check generates prioritized recommendations:
|
|
87
|
+
|
|
88
|
+
- **CRITICAL**: Blockchain integrity failure
|
|
89
|
+
- **HIGH**: Missing L0 approval workflow
|
|
90
|
+
- **MEDIUM**: Low health scores on individual knowledge entries
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
module KairosMcp
|
|
6
|
+
module SkillSets
|
|
7
|
+
module Introspection
|
|
8
|
+
# HealthScorer calculates health scores for L1 knowledge entries.
|
|
9
|
+
#
|
|
10
|
+
# When Synoptis TrustScorer is available, health is a weighted composite
|
|
11
|
+
# of trust score (70%) and staleness score (30%). When TrustScorer is
|
|
12
|
+
# unavailable (Synoptis not loaded), health equals staleness only.
|
|
13
|
+
#
|
|
14
|
+
# Staleness is computed from File.mtime of the knowledge .md file,
|
|
15
|
+
# decaying linearly over a configurable threshold (default 180 days).
|
|
16
|
+
class HealthScorer
|
|
17
|
+
TRUST_WEIGHT = 0.70
|
|
18
|
+
STALENESS_WEIGHT = 0.30
|
|
19
|
+
|
|
20
|
+
def initialize(user_context: nil, config: {})
|
|
21
|
+
@user_context = user_context
|
|
22
|
+
@config = config
|
|
23
|
+
@trust_scorer = build_trust_scorer
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Score all L1 knowledge entries.
|
|
27
|
+
#
|
|
28
|
+
# @return [Hash] :overall_health, :entry_count, :trust_scorer_available, :entries
|
|
29
|
+
def score_l1
|
|
30
|
+
provider = ::KairosMcp::KnowledgeProvider.new(nil, user_context: @user_context)
|
|
31
|
+
summaries = provider.list
|
|
32
|
+
|
|
33
|
+
scored = summaries.map { |summary| score_entry_from_summary(summary, provider) }
|
|
34
|
+
.sort_by { |e| e[:health_score] }
|
|
35
|
+
|
|
36
|
+
overall = scored.empty? ? 0.0 : (scored.sum { |e| e[:health_score] } / scored.size).round(4)
|
|
37
|
+
|
|
38
|
+
{
|
|
39
|
+
overall_health: overall,
|
|
40
|
+
entry_count: scored.size,
|
|
41
|
+
trust_scorer_available: !@trust_scorer.nil?,
|
|
42
|
+
entries: scored
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Score a single L1 knowledge entry by name.
|
|
47
|
+
#
|
|
48
|
+
# @param name [String] Knowledge entry name
|
|
49
|
+
# @return [Hash] :entry or :error
|
|
50
|
+
def score_single(name)
|
|
51
|
+
provider = ::KairosMcp::KnowledgeProvider.new(nil, user_context: @user_context)
|
|
52
|
+
entry = provider.get(name)
|
|
53
|
+
return { error: "Knowledge '#{name}' not found" } unless entry
|
|
54
|
+
|
|
55
|
+
{
|
|
56
|
+
entry: score_entry_from_skill_entry(entry),
|
|
57
|
+
trust_scorer_available: !@trust_scorer.nil?
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# Score from a summary hash (from provider.list).
|
|
64
|
+
# Must call provider.get(name) to obtain SkillEntry with md_file_path.
|
|
65
|
+
def score_entry_from_summary(summary, provider)
|
|
66
|
+
skill_entry = provider.get(summary[:name])
|
|
67
|
+
if skill_entry
|
|
68
|
+
score_entry_from_skill_entry(skill_entry)
|
|
69
|
+
else
|
|
70
|
+
# Fallback: no SkillEntry found (shouldn't happen normally)
|
|
71
|
+
{
|
|
72
|
+
name: summary[:name],
|
|
73
|
+
health_score: 0.5,
|
|
74
|
+
trust_score: 0.0,
|
|
75
|
+
trust_details: {},
|
|
76
|
+
attestation_count: 0,
|
|
77
|
+
staleness_score: 0.5,
|
|
78
|
+
tags: summary[:tags]
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Score from a SkillEntry object (has md_file_path).
|
|
84
|
+
def score_entry_from_skill_entry(entry)
|
|
85
|
+
trust = if @trust_scorer
|
|
86
|
+
@trust_scorer.calculate("knowledge://#{entry.name}")
|
|
87
|
+
else
|
|
88
|
+
{ score: 0.0, details: {}, attestation_count: 0, active_count: 0 }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
staleness = calculate_staleness(entry)
|
|
92
|
+
|
|
93
|
+
# When TrustScorer unavailable, health = staleness only
|
|
94
|
+
health = if @trust_scorer
|
|
95
|
+
(trust[:score] * TRUST_WEIGHT + staleness * STALENESS_WEIGHT).round(4)
|
|
96
|
+
else
|
|
97
|
+
staleness.round(4)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
{
|
|
101
|
+
name: entry.name,
|
|
102
|
+
health_score: health,
|
|
103
|
+
trust_score: trust[:score],
|
|
104
|
+
trust_details: trust[:details],
|
|
105
|
+
attestation_count: trust[:attestation_count] || 0,
|
|
106
|
+
staleness_score: staleness.round(4),
|
|
107
|
+
tags: entry.tags
|
|
108
|
+
}
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Calculate staleness score from file modification time.
|
|
112
|
+
# Returns 1.0 for freshly modified, decays to 0.0 over threshold days.
|
|
113
|
+
def calculate_staleness(entry)
|
|
114
|
+
md_path = entry.respond_to?(:md_file_path) ? entry.md_file_path : nil
|
|
115
|
+
return 0.5 unless md_path && File.exist?(md_path)
|
|
116
|
+
|
|
117
|
+
age_days = (Time.now - File.mtime(md_path)) / 86400.0
|
|
118
|
+
threshold = @config.dig('introspection', 'health', 'staleness_days') || 180
|
|
119
|
+
[1.0 - (age_days / threshold.to_f), 0.0].max
|
|
120
|
+
rescue StandardError
|
|
121
|
+
0.5
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def build_trust_scorer
|
|
125
|
+
return nil unless defined?(::Synoptis::TrustScorer)
|
|
126
|
+
registry = ::Synoptis::Registry::FileRegistry.new(
|
|
127
|
+
storage_dir: File.join(::KairosMcp.storage_dir, 'synoptis')
|
|
128
|
+
)
|
|
129
|
+
::Synoptis::TrustScorer.new(registry: registry)
|
|
130
|
+
rescue StandardError
|
|
131
|
+
nil
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
module KairosMcp
|
|
6
|
+
module SkillSets
|
|
7
|
+
module Introspection
|
|
8
|
+
# SafetyInspector reports on all active safety mechanisms across layers:
|
|
9
|
+
# - L0: approval_workflow (Kairos DSL)
|
|
10
|
+
# - RBAC: registered Safety policies
|
|
11
|
+
# - Agent: autonomous mode safety gates from agent.yml
|
|
12
|
+
# - Blockchain: chain integrity and block count
|
|
13
|
+
class SafetyInspector
|
|
14
|
+
# Inspect all safety layers and return a structured report.
|
|
15
|
+
#
|
|
16
|
+
# @return [Hash] :layers with 4 sub-keys
|
|
17
|
+
def inspect_safety
|
|
18
|
+
{
|
|
19
|
+
layers: {
|
|
20
|
+
l0_approval_workflow: inspect_approval_workflow,
|
|
21
|
+
runtime_rbac: inspect_rbac_policies,
|
|
22
|
+
agent_safety_gates: inspect_agent_gates,
|
|
23
|
+
blockchain_recording: inspect_blockchain_health
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def inspect_approval_workflow
|
|
31
|
+
if defined?(::Kairos) && ::Kairos.respond_to?(:skill)
|
|
32
|
+
skill = ::Kairos.skill(:approval_workflow)
|
|
33
|
+
{
|
|
34
|
+
present: !skill.nil?,
|
|
35
|
+
version: skill&.respond_to?(:version) ? skill.version : nil,
|
|
36
|
+
status: skill ? 'active' : 'not_loaded'
|
|
37
|
+
}
|
|
38
|
+
else
|
|
39
|
+
{ present: false, status: 'kairos_not_available' }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def inspect_rbac_policies
|
|
44
|
+
names = ::KairosMcp::Safety.registered_policy_names
|
|
45
|
+
{
|
|
46
|
+
registered_count: names.size,
|
|
47
|
+
policies: names,
|
|
48
|
+
multiuser_active: names.include?('can_modify_l0')
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def inspect_agent_gates
|
|
53
|
+
agent_config_path = File.join(::KairosMcp.skillsets_dir, 'agent', 'config', 'agent.yml')
|
|
54
|
+
if File.exist?(agent_config_path)
|
|
55
|
+
config = YAML.safe_load(File.read(agent_config_path)) || {}
|
|
56
|
+
autonomous = config['autonomous'] || {}
|
|
57
|
+
{
|
|
58
|
+
present: true,
|
|
59
|
+
max_cycles: autonomous['max_cycles'],
|
|
60
|
+
timeout: autonomous['timeout'],
|
|
61
|
+
max_llm_calls: autonomous['max_llm_calls'],
|
|
62
|
+
risk_budget: autonomous['risk_budget']
|
|
63
|
+
}
|
|
64
|
+
else
|
|
65
|
+
{ present: false, status: 'agent_skillset_not_loaded' }
|
|
66
|
+
end
|
|
67
|
+
rescue StandardError => e
|
|
68
|
+
{ present: false, error: e.message }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def inspect_blockchain_health
|
|
72
|
+
chain = ::KairosMcp::KairosChain::Chain.new
|
|
73
|
+
blocks = chain.chain
|
|
74
|
+
{
|
|
75
|
+
block_count: blocks.size,
|
|
76
|
+
last_recorded: blocks.last&.timestamp&.iso8601,
|
|
77
|
+
integrity: chain.valid?
|
|
78
|
+
}
|
|
79
|
+
rescue StandardError => e
|
|
80
|
+
{ block_count: 0, integrity: false, error: e.message }
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "introspection",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Self-inspection and maintenance. Calculates knowledge health scores, checks blockchain integrity, and visualizes safety mechanisms.",
|
|
5
|
+
"author": "Masaomi Hatakeyama",
|
|
6
|
+
"layer": "L1",
|
|
7
|
+
"depends_on": [],
|
|
8
|
+
"provides": ["health_scoring", "integrity_check", "safety_visibility"],
|
|
9
|
+
"tool_classes": [
|
|
10
|
+
"KairosMcp::SkillSets::Introspection::Tools::IntrospectionCheck",
|
|
11
|
+
"KairosMcp::SkillSets::Introspection::Tools::IntrospectionHealth",
|
|
12
|
+
"KairosMcp::SkillSets::Introspection::Tools::IntrospectionSafety"
|
|
13
|
+
],
|
|
14
|
+
"config_files": ["config/introspection.yml"],
|
|
15
|
+
"knowledge_dirs": ["knowledge/introspection_guide"],
|
|
16
|
+
"min_core_version": "3.9.0"
|
|
17
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
|
|
6
|
+
module KairosMcp
|
|
7
|
+
module SkillSets
|
|
8
|
+
module Introspection
|
|
9
|
+
module Tools
|
|
10
|
+
# Full self-inspection tool combining health, blockchain, and safety domains.
|
|
11
|
+
# Produces a consolidated report with recommendations.
|
|
12
|
+
class IntrospectionCheck < ::KairosMcp::Tools::BaseTool
|
|
13
|
+
def name
|
|
14
|
+
'introspection_check'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def description
|
|
18
|
+
'Full self-inspection: knowledge health scores, blockchain integrity, ' \
|
|
19
|
+
'and safety mechanism visibility. Uses Synoptis TrustScorer when available.'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def category
|
|
23
|
+
:introspection
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def usecase_tags
|
|
27
|
+
%w[health blockchain safety introspection maintenance]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def related_tools
|
|
31
|
+
%w[introspection_health introspection_safety chain_verify]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def input_schema
|
|
35
|
+
{
|
|
36
|
+
type: 'object',
|
|
37
|
+
properties: {
|
|
38
|
+
domains: {
|
|
39
|
+
type: 'array',
|
|
40
|
+
items: { type: 'string', enum: %w[health blockchain safety] },
|
|
41
|
+
description: 'Domains to inspect (default: all)'
|
|
42
|
+
},
|
|
43
|
+
format: {
|
|
44
|
+
type: 'string',
|
|
45
|
+
enum: %w[markdown json],
|
|
46
|
+
description: 'Output format (default: markdown)'
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def call(arguments)
|
|
53
|
+
domains = arguments['domains'] || %w[health blockchain safety]
|
|
54
|
+
fmt = arguments['format'] || 'markdown'
|
|
55
|
+
|
|
56
|
+
report = { inspected_at: Time.now.iso8601 }
|
|
57
|
+
|
|
58
|
+
if domains.include?('health')
|
|
59
|
+
report[:health] = health_scorer.score_l1
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
if domains.include?('blockchain')
|
|
63
|
+
report[:blockchain] = check_blockchain
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
if domains.include?('safety')
|
|
67
|
+
report[:safety] = safety_inspector.inspect_safety
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
report[:recommendations] = build_recommendations(report)
|
|
71
|
+
|
|
72
|
+
if fmt == 'json'
|
|
73
|
+
text_content(JSON.pretty_generate(report))
|
|
74
|
+
else
|
|
75
|
+
text_content(format_markdown(report))
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def health_scorer
|
|
82
|
+
@health_scorer ||= HealthScorer.new(
|
|
83
|
+
user_context: @safety&.current_user,
|
|
84
|
+
config: load_config
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def safety_inspector
|
|
89
|
+
@safety_inspector ||= SafetyInspector.new
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def check_blockchain
|
|
93
|
+
chain = ::KairosMcp::KairosChain::Chain.new
|
|
94
|
+
valid = chain.valid?
|
|
95
|
+
blocks = chain.chain
|
|
96
|
+
{
|
|
97
|
+
valid: valid,
|
|
98
|
+
block_count: blocks.size,
|
|
99
|
+
latest_timestamp: blocks.last&.timestamp&.iso8601,
|
|
100
|
+
status: valid ? 'healthy' : 'INTEGRITY_FAILURE'
|
|
101
|
+
}
|
|
102
|
+
rescue StandardError => e
|
|
103
|
+
{ valid: false, error: e.message, status: 'error' }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def build_recommendations(report)
|
|
107
|
+
recs = []
|
|
108
|
+
|
|
109
|
+
# Low health scores
|
|
110
|
+
if report[:health]
|
|
111
|
+
report[:health][:entries]&.each do |entry|
|
|
112
|
+
if entry[:health_score] < 0.4
|
|
113
|
+
recs << {
|
|
114
|
+
priority: 'medium',
|
|
115
|
+
target: entry[:name],
|
|
116
|
+
message: "Low health score (#{entry[:health_score]}). Consider updating or adding attestations."
|
|
117
|
+
}
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Blockchain issues
|
|
123
|
+
if report[:blockchain] && !report[:blockchain][:valid]
|
|
124
|
+
recs << { priority: 'critical', target: 'blockchain', message: 'Blockchain integrity check failed.' }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Safety gaps
|
|
128
|
+
if report[:safety]
|
|
129
|
+
unless report.dig(:safety, :layers, :l0_approval_workflow, :present)
|
|
130
|
+
recs << { priority: 'high', target: 'approval_workflow', message: 'L0 approval workflow not loaded.' }
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
recs
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def load_config
|
|
138
|
+
config_path = File.join(
|
|
139
|
+
::KairosMcp.skillsets_dir, 'introspection', 'config', 'introspection.yml'
|
|
140
|
+
)
|
|
141
|
+
return {} unless File.exist?(config_path)
|
|
142
|
+
YAML.safe_load(File.read(config_path)) || {}
|
|
143
|
+
rescue StandardError
|
|
144
|
+
{}
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def format_markdown(report)
|
|
148
|
+
lines = []
|
|
149
|
+
lines << "# Introspection Report"
|
|
150
|
+
lines << ""
|
|
151
|
+
lines << "**Inspected at**: #{report[:inspected_at]}"
|
|
152
|
+
lines << ""
|
|
153
|
+
|
|
154
|
+
if report[:health]
|
|
155
|
+
lines << "## Knowledge Health"
|
|
156
|
+
lines << ""
|
|
157
|
+
lines << "- **Overall health**: #{report[:health][:overall_health]}"
|
|
158
|
+
lines << "- **Entry count**: #{report[:health][:entry_count]}"
|
|
159
|
+
lines << "- **TrustScorer available**: #{report[:health][:trust_scorer_available]}"
|
|
160
|
+
lines << ""
|
|
161
|
+
|
|
162
|
+
if report[:health][:entries]&.any?
|
|
163
|
+
lines << "| Name | Health | Trust | Staleness | Attestations |"
|
|
164
|
+
lines << "|------|--------|-------|-----------|--------------|"
|
|
165
|
+
report[:health][:entries].each do |e|
|
|
166
|
+
lines << "| #{e[:name]} | #{e[:health_score]} | #{e[:trust_score]} | #{e[:staleness_score]} | #{e[:attestation_count]} |"
|
|
167
|
+
end
|
|
168
|
+
lines << ""
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
if report[:blockchain]
|
|
173
|
+
lines << "## Blockchain"
|
|
174
|
+
lines << ""
|
|
175
|
+
lines << "- **Valid**: #{report[:blockchain][:valid]}"
|
|
176
|
+
lines << "- **Block count**: #{report[:blockchain][:block_count]}"
|
|
177
|
+
lines << "- **Latest timestamp**: #{report[:blockchain][:latest_timestamp] || 'N/A'}"
|
|
178
|
+
lines << "- **Status**: #{report[:blockchain][:status]}"
|
|
179
|
+
lines << ""
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
if report[:safety]
|
|
183
|
+
lines << "## Safety Mechanisms"
|
|
184
|
+
lines << ""
|
|
185
|
+
report[:safety][:layers]&.each do |layer_name, layer_data|
|
|
186
|
+
lines << "### #{layer_name}"
|
|
187
|
+
layer_data.each { |k, v| lines << "- **#{k}**: #{v}" }
|
|
188
|
+
lines << ""
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
if report[:recommendations]&.any?
|
|
193
|
+
lines << "## Recommendations"
|
|
194
|
+
lines << ""
|
|
195
|
+
report[:recommendations].each do |rec|
|
|
196
|
+
lines << "- [#{rec[:priority].upcase}] **#{rec[:target]}**: #{rec[:message]}"
|
|
197
|
+
end
|
|
198
|
+
lines << ""
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
lines.join("\n")
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module KairosMcp
|
|
6
|
+
module SkillSets
|
|
7
|
+
module Introspection
|
|
8
|
+
module Tools
|
|
9
|
+
# Calculate health scores for L1 knowledge entries.
|
|
10
|
+
# Uses Synoptis TrustScorer when available, falls back to staleness-only scoring.
|
|
11
|
+
class IntrospectionHealth < ::KairosMcp::Tools::BaseTool
|
|
12
|
+
def name
|
|
13
|
+
'introspection_health'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def description
|
|
17
|
+
'Calculate health scores for L1 knowledge entries. ' \
|
|
18
|
+
'Uses Synoptis TrustScorer when available, falls back to staleness-only scoring.'
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def category
|
|
22
|
+
:introspection
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def usecase_tags
|
|
26
|
+
%w[health knowledge staleness trust introspection]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def related_tools
|
|
30
|
+
%w[introspection_check knowledge_list]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def input_schema
|
|
34
|
+
{
|
|
35
|
+
type: 'object',
|
|
36
|
+
properties: {
|
|
37
|
+
name: { type: 'string', description: 'Specific L1 knowledge name (optional, omit for all)' },
|
|
38
|
+
sort_by: { type: 'string', enum: %w[score name], description: 'Sort order (default: score)' },
|
|
39
|
+
below_threshold: { type: 'number', description: 'Only show entries below this score (0.0-1.0)' }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def call(arguments)
|
|
45
|
+
scorer = HealthScorer.new(user_context: @safety&.current_user, config: load_config)
|
|
46
|
+
result = if arguments['name']
|
|
47
|
+
scorer.score_single(arguments['name'])
|
|
48
|
+
else
|
|
49
|
+
scorer.score_l1
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Filter
|
|
53
|
+
if arguments['below_threshold'] && result[:entries]
|
|
54
|
+
result[:entries].select! { |e| e[:health_score] < arguments['below_threshold'] }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Sort
|
|
58
|
+
if arguments['sort_by'] == 'name' && result[:entries]
|
|
59
|
+
result[:entries].sort_by! { |e| e[:name] }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
text_content(JSON.pretty_generate(result))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def load_config
|
|
68
|
+
config_path = File.join(
|
|
69
|
+
KairosMcp.skillsets_dir, 'introspection', 'config', 'introspection.yml'
|
|
70
|
+
)
|
|
71
|
+
return {} unless File.exist?(config_path)
|
|
72
|
+
YAML.safe_load(File.read(config_path)) || {}
|
|
73
|
+
rescue StandardError
|
|
74
|
+
{}
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module KairosMcp
|
|
6
|
+
module SkillSets
|
|
7
|
+
module Introspection
|
|
8
|
+
module Tools
|
|
9
|
+
# Visualize all active safety mechanisms across layers:
|
|
10
|
+
# L0 approval workflow, RBAC policies, agent safety gates, blockchain health.
|
|
11
|
+
class IntrospectionSafety < ::KairosMcp::Tools::BaseTool
|
|
12
|
+
def name
|
|
13
|
+
'introspection_safety'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def description
|
|
17
|
+
'Visualize all active safety mechanisms across layers: ' \
|
|
18
|
+
'L0 approval workflow, RBAC policies, agent safety gates, blockchain health.'
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def category
|
|
22
|
+
:introspection
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def usecase_tags
|
|
26
|
+
%w[safety rbac approval blockchain introspection]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def related_tools
|
|
30
|
+
%w[introspection_check chain_verify]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def input_schema
|
|
34
|
+
{ type: 'object', properties: {} }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def call(_arguments)
|
|
38
|
+
inspector = SafetyInspector.new
|
|
39
|
+
result = inspector.inspect_safety
|
|
40
|
+
text_content(JSON.pretty_generate(result))
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kairos-chain
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Masaomi Hatakeyama
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-03-
|
|
11
|
+
date: 2026-03-31 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: minitest
|
|
@@ -299,6 +299,15 @@ files:
|
|
|
299
299
|
- templates/skillsets/hestia/tools/meeting_publish_needs.rb
|
|
300
300
|
- templates/skillsets/hestia/tools/philosophy_anchor.rb
|
|
301
301
|
- templates/skillsets/hestia/tools/record_observation.rb
|
|
302
|
+
- templates/skillsets/introspection/config/introspection.yml
|
|
303
|
+
- templates/skillsets/introspection/knowledge/introspection_guide/introspection_guide.md
|
|
304
|
+
- templates/skillsets/introspection/lib/introspection.rb
|
|
305
|
+
- templates/skillsets/introspection/lib/introspection/health_scorer.rb
|
|
306
|
+
- templates/skillsets/introspection/lib/introspection/safety_inspector.rb
|
|
307
|
+
- templates/skillsets/introspection/skillset.json
|
|
308
|
+
- templates/skillsets/introspection/tools/introspection_check.rb
|
|
309
|
+
- templates/skillsets/introspection/tools/introspection_health.rb
|
|
310
|
+
- templates/skillsets/introspection/tools/introspection_safety.rb
|
|
302
311
|
- templates/skillsets/knowledge_creator/config/knowledge_creator.yml
|
|
303
312
|
- templates/skillsets/knowledge_creator/knowledge/creation_guide/creation_guide.md
|
|
304
313
|
- templates/skillsets/knowledge_creator/knowledge/quality_criteria/quality_criteria.md
|
|
@@ -462,7 +471,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
462
471
|
- !ruby/object:Gem::Version
|
|
463
472
|
version: '0'
|
|
464
473
|
requirements: []
|
|
465
|
-
rubygems_version: 3.
|
|
474
|
+
rubygems_version: 3.5.22
|
|
466
475
|
signing_key:
|
|
467
476
|
specification_version: 4
|
|
468
477
|
summary: KairosChain - Self-referential MCP server for auditable skill self-management
|