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 =
'1.0'- MAX_PERMISSIONS =
0o600- DEFAULT_FILENAME =
'rosett-ai-mcp-keys.yml'- EXPIRY_WARNING_DAYS =
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)
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!
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.
23 24 25 26 27 |
# File 'lib/rosett_ai/mcp/keyfile.rb', line 23 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.
131 132 133 134 135 136 137 138 139 140 |
# File 'lib/rosett_ai/mcp/keyfile.rb', line 131 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)
This method returns an undefined value.
Add a new key to the keyfile.
81 82 83 84 85 86 87 88 89 90 91 92 93 |
# File 'lib/rosett_ai/mcp/keyfile.rb', line 81 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).
69 70 71 72 73 |
# File 'lib/rosett_ai/mcp/keyfile.rb', line 69 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.
55 56 57 58 59 60 61 62 63 64 |
# File 'lib/rosett_ai/mcp/keyfile.rb', line 55 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.
33 34 35 36 37 38 39 40 |
# File 'lib/rosett_ai/mcp/keyfile.rb', line 33 def load! @mutex.synchronize do validate_file_exists parse_keyfile end self end |
#reload!
This method returns an undefined value.
Thread-safe reload (for SIGHUP handler).
45 46 47 48 49 |
# File 'lib/rosett_ai/mcp/keyfile.rb', line 45 def reload! load! rescue StandardError => e warn "[rai-mcp] Keyfile reload failed: #{e.}" end |
#revoke_key(name) ⇒ Boolean
Revoke (disable) a key by name.
99 100 101 102 103 104 105 106 107 108 |
# File 'lib/rosett_ai/mcp/keyfile.rb', line 99 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.
114 115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/rosett_ai/mcp/keyfile.rb', line 114 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 |