Class: RosettAi::Mcp::Keyfile
- Inherits:
-
Object
- Object
- RosettAi::Mcp::Keyfile
- Defined in:
- lib/rosett_ai/mcp/keyfile.rb
Overview
Multi-key authentication store with thread-safe reload.
Manages a YAML keyfile with salted hashes, expiry dates, and enable/disable flags. File permissions are enforced at 0600 or 0400.
Constant Summary collapse
- VERSION =
Returns Current version of the rosett-ai gem.
'1.0'- MAX_PERMISSIONS =
Returns Maximum number of permissions per key.
0o600- DEFAULT_FILENAME =
Returns Default filename for the MCP keyfile.
'rosett-ai-mcp-keys.yml'- EXPIRY_WARNING_DAYS =
Returns Days before expiry to show warnings.
7
Class Method Summary collapse
-
.create_if_missing(path) ⇒ String
Create an empty keyfile if missing.
Instance Method Summary collapse
-
#add_key(name:, key_hash:, expires_at: nil) ⇒ void
Add a new key to the keyfile.
-
#enabled_keys ⇒ Array<Hash>
List enabled keys (without hashes).
-
#find_key(plaintext) ⇒ Hash?
Find a key entry matching the given plaintext.
-
#initialize(path) ⇒ Keyfile
constructor
A new instance of Keyfile.
-
#load! ⇒ self
Load and validate the keyfile.
-
#reload! ⇒ void
Thread-safe reload (for SIGHUP handler).
-
#revoke_key(name) ⇒ Boolean
Revoke (disable) a key by name.
-
#rotate_key(name) ⇒ Hash?
Rotate a key: revoke old, return new hash for replacement.
Constructor Details
#initialize(path) ⇒ Keyfile
Returns a new instance of Keyfile.
27 28 29 30 31 |
# File 'lib/rosett_ai/mcp/keyfile.rb', line 27 def initialize(path) @path = File.(path) @mutex = Mutex.new @keys = [] end |
Class Method Details
.create_if_missing(path) ⇒ String
Create an empty keyfile if missing.
135 136 137 138 139 140 141 142 143 144 |
# File 'lib/rosett_ai/mcp/keyfile.rb', line 135 def self.create_if_missing(path) = File.(path) unless File.exist?() dir = File.dirname() FileUtils.mkdir_p(dir) File.write(, { 'version' => VERSION, 'keys' => [] }.to_yaml) File.chmod(0o600, ) end end |
Instance Method Details
#add_key(name:, key_hash:, expires_at: nil) ⇒ void
This method returns an undefined value.
Add a new key to the keyfile.
85 86 87 88 89 90 91 92 93 94 95 96 97 |
# File 'lib/rosett_ai/mcp/keyfile.rb', line 85 def add_key(name:, key_hash:, expires_at: nil) @mutex.synchronize do @keys << { name: name, algo: 'sha256_salt', key_hash: key_hash, created_at: Time.now.strftime('%Y-%m-%d'), expires_at: expires_at, enabled: true } write_keyfile end end |
#enabled_keys ⇒ Array<Hash>
List enabled keys (without hashes).
73 74 75 76 77 |
# File 'lib/rosett_ai/mcp/keyfile.rb', line 73 def enabled_keys @mutex.synchronize do @keys.select { |k| k[:enabled] }.map { |k| safe_entry(k) } end end |
#find_key(plaintext) ⇒ Hash?
Find a key entry matching the given plaintext.
59 60 61 62 63 64 65 66 67 68 |
# File 'lib/rosett_ai/mcp/keyfile.rb', line 59 def find_key(plaintext) @mutex.synchronize do @keys.find do |entry| next unless entry[:enabled] next if expired?(entry) KeyHasher.verify_key(plaintext, entry[:key_hash]) end end end |
#load! ⇒ self
Load and validate the keyfile.
37 38 39 40 41 42 43 44 |
# File 'lib/rosett_ai/mcp/keyfile.rb', line 37 def load! @mutex.synchronize do validate_file_exists parse_keyfile end self end |
#reload! ⇒ void
This method returns an undefined value.
Thread-safe reload (for SIGHUP handler).
49 50 51 52 53 |
# File 'lib/rosett_ai/mcp/keyfile.rb', line 49 def reload! load! rescue StandardError => e warn "[rai-mcp] Keyfile reload failed: #{e.}" end |
#revoke_key(name) ⇒ Boolean
Revoke (disable) a key by name.
103 104 105 106 107 108 109 110 111 112 |
# File 'lib/rosett_ai/mcp/keyfile.rb', line 103 def revoke_key(name) @mutex.synchronize do entry = @keys.find { |k| k[:name] == name } return false unless entry entry[:enabled] = false write_keyfile true end end |
#rotate_key(name) ⇒ Hash?
Rotate a key: revoke old, return new hash for replacement.
118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/rosett_ai/mcp/keyfile.rb', line 118 def rotate_key(name) @mutex.synchronize do old = @keys.find { |k| k[:name] == name } return nil unless old old[:enabled] = false new_key, new_entry = build_replacement_key(name, old[:expires_at]) @keys << new_entry write_keyfile { plaintext: new_key, entry: safe_entry(new_entry) } end end |