Class: KairosMcp::Daemon::Credentials
- Inherits:
-
Object
- Object
- KairosMcp::Daemon::Credentials
- Defined in:
- lib/kairos_mcp/daemon/credentials.rb
Overview
Credentials — Scoped, auto-redacting secret store for daemon-mode tools.
Design (v0.2 §P2.6):
A daemon running unattended needs access to API keys (ANTHROPIC_API_KEY,
GITHUB_TOKEN, ...) but each tool should only see the credentials it
actually needs. secrets.yml declares, per secret, which tool names may
request it; `fetch_for(tool_name)` returns only the matching subset.
Pattern matching uses File.fnmatch so patterns like 'llm_*' or
'safe_http_*' work the same way as in InvocationContext.
Secret sources:
env — value = ENV[spec['env_var']]
file — value = File.read(spec['file_path']).strip
keychain — stub; logs a warning and returns nil.
Safety:
• Empty or missing scoped_to means the secret is NOT handed to any tool.
• Missing env var / unreadable file → value is nil, not an exception,
so a misconfigured secret never leaks a filesystem path or ENV name
via a backtrace.
• `redact(str)` replaces every known secret *value* with '***REDACTED***'
so log lines and exception messages can be scrubbed before they
escape the daemon.
Thread-safety:
`reload!` can race with in-flight `fetch_for` calls from signal
handlers (SIGHUP). A Monitor guards the mutable state.
Constant Summary collapse
- REDACTED =
'***REDACTED***'
Instance Method Summary collapse
-
#all_patterns ⇒ Array<String>
Flat list of every scoped_to pattern across all specs, for diagnostics.
-
#fetch_for(tool_name) ⇒ Hash{String => String}
Return the subset of secrets whose scoped_to pattern matches ‘tool_name`.
-
#initialize(logger: nil) ⇒ Credentials
constructor
A new instance of Credentials.
-
#load(secrets_path) ⇒ self
Parse secrets.yml.
-
#redact(str) ⇒ String?
Replace every known secret value in ‘str` with REDACTED.
-
#reload! ⇒ self
Re-read the previously loaded secrets.yml.
-
#secret_names ⇒ Array<String>
Names of loaded secrets (for diagnostics).
Constructor Details
#initialize(logger: nil) ⇒ Credentials
Returns a new instance of Credentials.
40 41 42 43 44 45 46 |
# File 'lib/kairos_mcp/daemon/credentials.rb', line 40 def initialize(logger: nil) @logger = logger @monitor = Monitor.new @path = nil @specs = [] # Array<Hash> — normalized secret specs @values = {} # { name => String } — resolved values end |
Instance Method Details
#all_patterns ⇒ Array<String>
Flat list of every scoped_to pattern across all specs, for diagnostics.
113 114 115 116 117 |
# File 'lib/kairos_mcp/daemon/credentials.rb', line 113 def all_patterns @monitor.synchronize do @specs.flat_map { |s| Array(s['scoped_to']) }.map(&:to_s).uniq end end |
#fetch_for(tool_name) ⇒ Hash{String => String}
Return the subset of secrets whose scoped_to pattern matches ‘tool_name`. Only *resolved, non-nil* values are included.
77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/kairos_mcp/daemon/credentials.rb', line 77 def fetch_for(tool_name) name = tool_name.to_s out = {} @monitor.synchronize do @specs.each do |spec| next unless matches?(spec, name) value = @values[spec['name']] next if value.nil? || value.empty? out[spec['name']] = value end end out end |
#load(secrets_path) ⇒ self
Parse secrets.yml. Non-existent path → empty (no crash).
51 52 53 54 55 56 57 58 |
# File 'lib/kairos_mcp/daemon/credentials.rb', line 51 def load(secrets_path) @monitor.synchronize do @path = secrets_path.to_s @specs = parse_file(@path) @values = resolve_all(@specs) end self end |
#redact(str) ⇒ String?
Replace every known secret value in ‘str` with REDACTED. Handles nil and empty input without raising so callers can pipe arbitrary log/exception strings through unconditionally.
97 98 99 100 101 102 103 104 105 106 107 108 109 |
# File 'lib/kairos_mcp/daemon/credentials.rb', line 97 def redact(str) return str if str.nil? return str if str.empty? out = str.dup @monitor.synchronize do # Replace longer values first so a secret that is a substring of # another does not get partially redacted. @values.values.compact.reject(&:empty?) .sort_by { |v| -v.length } .each { |v| out.gsub!(v, REDACTED) } end out end |
#reload! ⇒ self
Re-read the previously loaded secrets.yml. No-op if load was never called. Called from the SIGHUP handler.
63 64 65 66 67 68 69 70 |
# File 'lib/kairos_mcp/daemon/credentials.rb', line 63 def reload! @monitor.synchronize do return self if @path.nil? @specs = parse_file(@path) @values = resolve_all(@specs) end self end |
#secret_names ⇒ Array<String>
Names of loaded secrets (for diagnostics). Values are NOT exposed.
121 122 123 |
# File 'lib/kairos_mcp/daemon/credentials.rb', line 121 def secret_names @monitor.synchronize { @specs.map { |s| s['name'] }.compact } end |