Class: AgentsControl::Config
- Inherits:
-
Object
- Object
- AgentsControl::Config
- Defined in:
- lib/agents_control/config.rb
Overview
YAML settings at the XDG path.
Only non-secret data lives here. The bot token lives in the Keychain or libsecret and never lands in this file — the tool is meant to be published openly, and the config has to be safe to show anyone.
Constant Summary collapse
- DEFAULTS =
{ "telegram" => { # While the list is empty, the bot answers nobody. Filled in by # the setup wizard. Without this filter, anyone who found the bot # could approve command execution on this machine. "allowed_chat_ids" => [], "poll_timeout" => 30 }, "answers" => { # Answering "continue" on the user's behalf is safe. "auto_continue" => true, # Granting the agent a tool on the user's behalf is not. # Deliberately two separate settings: merged into one, they'd # produce an agent that approves itself everything while nobody # is watching. "auto_approve_permissions" => false, # Whether to intercept agent questions at all. While a human is # at the keyboard this only gets in the way: they'll answer in # the terminal faster, and a busy hook keeps the dialog from # ever reaching the screen. Turned on with the /away command. "away" => false, # Whether to also mirror questions to Telegram when interception # is off. "notify_when_present" => true, # How long to wait for a Telegram reply before declining. Claude # Code holds the hook open longer than the documented 600 # seconds, so this upper bound is ours, not its. "reply_timeout" => 900, # Automatically type a continuation once a session has hit a # rate limit and the limit has reset. Exactly as safe as # auto_continue — grants the agent no new permissions, just # clears idle time nobody got around to clearing by hand. "auto_resume_after_limit" => true, "resume_message" => "Continue where you left off — the previous attempt was rate limited.", # Never auto-approved, no matter what the settings above say. "never_auto_approve" => [ "rm -rf", "git push --force", "curl | sh", "curl | bash", ".env" ] }, "anchors" => { "enabled" => false, "mode" => "headless", # The five-hour window is shared across the account, but weekly # limits are tracked per model family — so anchoring with a # cheap model is the better deal. "model" => "haiku", "schedule" => ["07:00", "12:00", "17:00"], "days" => %w[mon tue wed thu fri], "skip_if_window_active" => true }, "terminal" => { # Empty means auto-detect. Otherwise iterm2 / tmux / none. "backend" => nil, "context_lines" => 80, # Watch for a session's local CLI menus (model switch, folder # trust) on screen and forward them to Telegram. Hooks can't see # these — they're the CLI's own response to a human command, not # a decision made by the agent. "watch_menus" => true, "menu_poll_interval" => 20, "rate_limit_poll_interval" => 60 } }.freeze
Instance Attribute Summary collapse
-
#path ⇒ Object
readonly
Returns the value of attribute path.
Class Method Summary collapse
Instance Method Summary collapse
-
#get(key, default = nil) ⇒ Object
Reads via a dotted path: config.get("anchors.model").
-
#initialize(data = {}, path: self.class.path) ⇒ Config
constructor
A new instance of Config.
-
#save ⇒ Object
Written through a temp file, chmod'd before the rename: the file may contain the allowed-chats list, and it must never be world-readable even for the brief moment between creation and the permission change landing on the final path.
- #set(key, value) ⇒ Object
- #to_h ⇒ Object
Constructor Details
Instance Attribute Details
#path ⇒ Object (readonly)
Returns the value of attribute path.
100 101 102 |
# File 'lib/agents_control/config.rb', line 100 def path @path end |
Class Method Details
.load(path = self.path) ⇒ Object
86 87 88 |
# File 'lib/agents_control/config.rb', line 86 def self.load(path = self.path) new(read(path), path: path) end |
.path ⇒ Object
81 82 83 84 |
# File 'lib/agents_control/config.rb', line 81 def self.path base = ENV["XDG_CONFIG_HOME"] || File.("~/.config") File.join(base, "agents_control", "config.yml") end |
.read(path) ⇒ Object
90 91 92 93 94 95 96 97 98 |
# File 'lib/agents_control/config.rb', line 90 def self.read(path) return {} unless File.exist?(path) YAML.safe_load_file(path, permitted_classes: [], aliases: false) || {} rescue Psych::SyntaxError # A broken config must not keep the daemon from starting: fall # back to defaults and flag it in doctor. {} end |
Instance Method Details
#get(key, default = nil) ⇒ Object
Reads via a dotted path: config.get("anchors.model").
108 109 110 111 112 113 114 |
# File 'lib/agents_control/config.rb', line 108 def get(key, default = nil) key.to_s.split(".").reduce(@data) do |node, part| return default unless node.is_a?(Hash) && node.key?(part) node[part] end end |
#save ⇒ Object
Written through a temp file, chmod'd before the rename: the file may contain the allowed-chats list, and it must never be world-readable even for the brief moment between creation and the permission change landing on the final path.
127 128 129 130 131 132 133 134 135 136 137 |
# File 'lib/agents_control/config.rb', line 127 def save FileUtils.mkdir_p(File.dirname(path)) temporary = "#{path}.#{Process.pid}.tmp" File.write(temporary, YAML.dump(@data)) File.chmod(0o600, temporary) File.rename(temporary, path) self ensure FileUtils.rm_f(temporary) if temporary && File.exist?(temporary) end |
#set(key, value) ⇒ Object
116 117 118 119 120 121 |
# File 'lib/agents_control/config.rb', line 116 def set(key, value) parts = key.to_s.split(".") leaf = parts[0..-2].reduce(@data) { |node, part| node[part] ||= {} } leaf[parts.last] = value self end |
#to_h ⇒ Object
139 |
# File 'lib/agents_control/config.rb', line 139 def to_h = @data |