Class: KairosMcp::ContextManager
- Inherits:
-
Object
- Object
- KairosMcp::ContextManager
- Defined in:
- lib/kairos_mcp/context_manager.rb
Overview
ContextManager: Manages L2 (context layer) skills in Anthropic format
L2 characteristics:
- Temporary context and hypotheses
- No blockchain recording (free modification)
- Session-based organization
Instance Method Summary collapse
-
#create_subdir(session_id, name, subdir) ⇒ Hash
Create a subdirectory (scripts, assets, or references).
-
#delete_context(session_id, name) ⇒ Hash
Delete a context.
-
#delete_session(session_id) ⇒ Hash
Delete an entire session.
-
#generate_session_id(prefix: 'session') ⇒ String
Generate a unique session ID.
-
#get_context(session_id, name) ⇒ AnthropicSkillParser::SkillEntry?
Get a specific context.
-
#initialize(context_dir = nil, user_context: nil) ⇒ ContextManager
constructor
A new instance of ContextManager.
-
#list_assets(session_id, name) ⇒ Array<Hash>
List assets in a context.
-
#list_contexts_in_session(session_id) ⇒ Array<Hash>
List all contexts in a session.
-
#list_scripts(session_id, name) ⇒ Array<Hash>
List scripts in a context.
-
#list_sessions ⇒ Array<Hash>
List all active sessions.
-
#save_context(session_id, name, content, create_subdirs: false) ⇒ Hash
Save a context (create or update).
-
#validate_relations_in_content!(content) ⇒ Object
Parse the incoming content's frontmatter, and if it carries relations (Context Graph Phase 1), enforce the v2.1 §1.1 schema rules and path-containment guard.
Constructor Details
#initialize(context_dir = nil, user_context: nil) ⇒ ContextManager
Returns a new instance of ContextManager.
19 20 21 22 23 24 |
# File 'lib/kairos_mcp/context_manager.rb', line 19 def initialize(context_dir = nil, user_context: nil) context_dir ||= KairosMcp.context_dir(user_context: user_context) @context_dir = context_dir @user_context = user_context FileUtils.mkdir_p(@context_dir) end |
Instance Method Details
#create_subdir(session_id, name, subdir) ⇒ Hash
Create a subdirectory (scripts, assets, or references)
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 |
# File 'lib/kairos_mcp/context_manager.rb', line 191 def create_subdir(session_id, name, subdir) valid_subdirs = %w[scripts assets references] unless valid_subdirs.include?(subdir) return { success: false, error: "Invalid subdir. Must be one of: #{valid_subdirs.join(', ')}" } end context_dir = File.join(@context_dir, session_id, name) unless segments?(session_id, name) && contained?(context_dir) && File.directory?(context_dir) return { success: false, error: "Context '#{name}' not found in session '#{session_id}'" } end subdir_path = File.join(context_dir, subdir) unless contained?(subdir_path) return { success: false, error: "Invalid subdir '#{subdir}': resolves outside the context directory" } end # mkdir_p raises Errno::EEXIST when the name is taken by a dangling # symlink, and this method's contract is a result hash, not an exception. FileUtils.mkdir_p(subdir_path) { success: true, path: subdir_path } rescue SystemCallError => e { success: false, error: "Could not create subdir '#{subdir}': #{e.}" } end |
#delete_context(session_id, name) ⇒ Hash
Delete a context
156 157 158 159 160 161 162 163 164 165 166 167 |
# File 'lib/kairos_mcp/context_manager.rb', line 156 def delete_context(session_id, name) context_dir = File.join(@context_dir, session_id, name) # rm_rf below: containment is checked first so a traversing name cannot # delete a tree outside the context store. unless segments?(session_id, name) && contained?(context_dir) && File.directory?(context_dir) return { success: false, error: "Context '#{name}' not found in session '#{session_id}'" } end FileUtils.rm_rf(context_dir) { success: true, deleted: name } end |
#delete_session(session_id) ⇒ Hash
Delete an entire session
173 174 175 176 177 178 179 180 181 182 183 |
# File 'lib/kairos_mcp/context_manager.rb', line 173 def delete_session(session_id) session_dir = File.join(@context_dir, session_id) unless segments?(session_id) && contained?(session_dir) && File.directory?(session_dir) return { success: false, error: "Session '#{session_id}' not found" } end contexts_count = context_dirs(session_dir).size FileUtils.rm_rf(session_dir) { success: true, deleted: session_id, contexts_deleted: contexts_count } end |
#generate_session_id(prefix: 'session') ⇒ String
Generate a unique session ID
219 220 221 222 223 |
# File 'lib/kairos_mcp/context_manager.rb', line 219 def generate_session_id(prefix: 'session') = Time.now.strftime('%Y%m%d_%H%M%S') random = SecureRandom.hex(4) "#{prefix}_#{}_#{random}" end |
#get_context(session_id, name) ⇒ AnthropicSkillParser::SkillEntry?
Get a specific context
72 73 74 75 76 77 78 79 80 |
# File 'lib/kairos_mcp/context_manager.rb', line 72 def get_context(session_id, name) return nil unless segments?(session_id, name) context_dir = File.join(@context_dir, session_id, name) return nil unless contained?(context_dir) return nil unless File.directory?(context_dir) AnthropicSkillParser.parse(context_dir) end |
#list_assets(session_id, name) ⇒ Array<Hash>
List assets in a context
242 243 244 245 246 247 |
# File 'lib/kairos_mcp/context_manager.rb', line 242 def list_assets(session_id, name) context = get_context(session_id, name) return [] unless context AnthropicSkillParser.list_assets(context) end |
#list_contexts_in_session(session_id) ⇒ Array<Hash>
List all contexts in a session
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
# File 'lib/kairos_mcp/context_manager.rb', line 46 def list_contexts_in_session(session_id) return [] unless segments?(session_id) session_dir = File.join(@context_dir, session_id) return [] unless contained?(session_dir) return [] unless File.directory?(session_dir) context_dirs(session_dir).map do |dir| skill = AnthropicSkillParser.parse(dir) next unless skill { name: skill.name, description: skill.description, has_scripts: skill.has_scripts?, has_assets: skill.has_assets?, has_references: skill.has_references? } end.compact end |
#list_scripts(session_id, name) ⇒ Array<Hash>
List scripts in a context
230 231 232 233 234 235 |
# File 'lib/kairos_mcp/context_manager.rb', line 230 def list_scripts(session_id, name) context = get_context(session_id, name) return [] unless context AnthropicSkillParser.list_scripts(context) end |
#list_sessions ⇒ Array<Hash>
List all active sessions
29 30 31 32 33 34 35 36 37 38 39 40 |
# File 'lib/kairos_mcp/context_manager.rb', line 29 def list_sessions session_dirs.map do |dir| session_id = File.basename(dir) contexts = list_contexts_in_session(session_id) { session_id: session_id, context_count: contexts.size, created_at: File.ctime(dir), modified_at: File.mtime(dir) } end.sort_by { |s| s[:modified_at] }.reverse end |
#save_context(session_id, name, content, create_subdirs: false) ⇒ Hash
Save a context (create or update)
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
# File 'lib/kairos_mcp/context_manager.rb', line 89 def save_context(session_id, name, content, create_subdirs: false) validate_relations_in_content!(content) session_dir = File.join(@context_dir, session_id) context_dir = File.join(session_dir, name) # Checked before mkdir_p, so neither value can create a directory outside # the store. Both guards are needed: containment bounds the composed leaf, # and the segment check bounds session_dir and the second join the parser # performs on `name` — paths this method never composes itself. unless segments?(session_id, name) && contained?(context_dir) return { success: false, error: "Invalid session_id/name: '#{session_id}/#{name}' resolves outside the context directory" } end FileUtils.mkdir_p(session_dir) if File.directory?(context_dir) # Update existing skill = AnthropicSkillParser.update(context_dir, content) { success: true, action: 'updated', context: skill.to_h } else # Create new skill = AnthropicSkillParser.create(session_dir, name, content, create_subdirs: create_subdirs) { success: true, action: 'created', context: skill.to_h } end rescue ContextGraph::Error => e { success: false, error: "#{e.class.name.split('::').last}: #{e.}" } rescue StandardError => e { success: false, error: e. } end |
#validate_relations_in_content!(content) ⇒ Object
Parse the incoming content's frontmatter, and if it carries relations (Context Graph Phase 1), enforce the v2.1 §1.1 schema rules and path-containment guard. Pure validation — does not mutate content.
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
# File 'lib/kairos_mcp/context_manager.rb', line 125 def validate_relations_in_content!(content) return unless content.is_a?(String) m = content.match(/\A---\r?\n(.+?)\r?\n---\r?\n/m) return unless m begin front = YAML.safe_load(m[1], permitted_classes: [Symbol, Date, Time]) || {} rescue StandardError => e raise ContextGraph::InvalidFrontmatterError, "frontmatter parse failed: #{e.}" end relations = front['relations'] || front[:relations] return if relations.nil? ContextGraph.validate_relations!(relations) # For each target, run the path-containment guard. PathEscape and # SymlinkRejected are hard fails on the write path. ENOENT (dangling) # is allowed (forward references are part of L2-evidential ontology). relations.each do |item| target = item['target'] || item[:target] ContextGraph.resolve_target(target, @context_dir) end end |