Class: Antigravity::Policy
- Inherits:
-
Object
- Object
- Antigravity::Policy
- Defined in:
- lib/antigravity/policy.rb,
lib/antigravity/policy/constants.rb
Overview
========================================================================== Antigravity::Policy — Declarative tool-access control for agents.
⚠️ ORDER DOES NOT MATTER!
The DSL is declarative, like SQL — not imperative like a script.
Rules are evaluated by PRECEDENCE, not by insertion order.
You can write allow before deny or vice versa — same result.
Precedence (highest wins):
1. Tool specificity: specific tool > wildcard (nil)
2. Condition specificity: has `when:` > no `when:`
3. Restrictiveness: deny > confirm > allow
Example — these two policies behave identically:
Policy.define do Policy.define do
allow :run_command deny :run_command, when: cmd('rm')
deny :run_command, allow :run_command
when: cmd('rm') end
end
In both cases, rm is denied (conditional deny beats unconditional
allow), and everything else is allowed.
See policy/constants.rb for curated command/file/tool lists.
Defined Under Namespace
Classes: Rule
Constant Summary collapse
- CATASTROPHIC_CMDS =
💀 Catastrophic commands — hard-denied in ALL presets, no exceptions.
[ 'dd if=/dev/urandom', 'dd if=/dev/zero', '> /dev/sd', 'halt', 'mkfs', 'reboot', 'rm -rf /*', 'rm -rf /', 'rm -rf ~', 'shutdown', ].freeze
- RISKY_CMDS =
⚠️ Risky commands — confirmed in :default, hard-denied in :cautious. Single-word or short patterns that match substring in command_line.
[ 'chmod -R 777', 'chown -R', 'kill -9', 'killall', 'pkill', 'rm', 'xargs', ].freeze
- DESTRUCTIVE_GIT_CMDS =
🔥 Destructive git — nuke local changes, rewrite history. Confirmed in :default/:turbo, hard-denied in :cautious/:test.
[ 'git checkout .', 'git checkout -- .', 'git clean -fd', 'git clean -fdx', 'git push --force', 'git push -f', 'git reset --hard', 'git stash drop', ].freeze
- SAFE_CMDS =
✅ Safe read-only shell commands — allowed even in :cautious.
%i[ cd date echo hostname ls md5 md5sum pwd uname wc which whoami ].freeze
- READ_CMDS =
⚠️ File-reading shell commands — can bypass view_file deny rules! Allowed in :default/:turbo/:test, but NOT in :cautious. If you deny
view_filefor a path,catcan circumvent it. %i[ cat head strings tail ].freeze
- SAFE_GIT_CMDS =
Safe git subcommands (read-only, no mutations)
[ 'git branch', 'git diff', 'git log', 'git remote', 'git status', ].freeze
- SENSITIVE_FILES =
🔐 Sensitive file globs — writes to these require confirmation.
[ '.env', '.env.*', '*.key', '*.pem', '*.secret', 'id_rsa*', ].freeze
- SANDBOX_DIRS =
📂 Sandbox directories — always writable, even in production. Throwaway / output dirs where agents can freely write.
[ 'out/*', 'scratch/*', ].freeze
- READONLY_TOOLS =
Read-only harness tools (always safe)
%i[ find grep_search list_dir read_url_content search_web view_file ].freeze
- WRITE_TOOLS =
Write harness tools
%i[ file_edit write_to_file ].freeze
- PRESET_NAMES =
🗺️ Environment → preset mapping (for Policy.auto)
%i[auto cautious default test turbo].freeze
- ENV_MAP =
{ 'dev' => :turbo, 'development' => :turbo, 'prod' => :cautious, 'production' => :cautious, 'staging' => :default, 'test' => :test, }.freeze
Class Method Summary collapse
- .allow_all ⇒ Object
-
.auto ⇒ Object
🔮 Auto — reads RAILS_ENV, RACK_ENV, or ANTIGRAVITY_ENV and picks a preset.
-
.cautious ⇒ Object
🔒 Cautious — read-only free, confirm everything else, hard-deny destructive.
-
.default ⇒ Object
⚖️ Default — balanced: allow reads + writes, confirm dangerous shell, protect sensitive files.
- .define(&block) ⇒ Object
- .deny_all ⇒ Object
-
.preset(name) ⇒ Policy
Resolve a preset by name (symbol).
-
.test ⇒ Object
🧪 Test — permissive for test runners, but sandboxed.
-
.turbo ⇒ Object
🚀 Turbo — wide open with seatbelts: allow everything, only hard-deny catastrophic.
Instance Method Summary collapse
-
#allow(tool_name = nil, **kwargs) ⇒ Object
------------------------------------------------------------------ DSL methods ------------------------------------------------------------------.
- #allow_all ⇒ Object
- #args_match(**matchers) ⇒ Object
-
#cmd(*patterns) ⇒ Object
------------------------------------------------------------------ Predicate helpers ------------------------------------------------------------------.
- #confirm(tool_name = nil, **kwargs, &block) ⇒ Object
- #deny(tool_name = nil, **kwargs) ⇒ Object
- #deny_all ⇒ Object
-
#evaluate(tool_name, args = {}) ⇒ Object
------------------------------------------------------------------ Evaluation engine ------------------------------------------------------------------.
-
#initialize(&block) ⇒ Policy
constructor
------------------------------------------------------------------ Constructor & factory methods ------------------------------------------------------------------.
- #on_confirm(&block) ⇒ Object
- #path(*globs) ⇒ Object
Constructor Details
#initialize(&block) ⇒ Policy
Constructor & factory methods
75 76 77 78 79 |
# File 'lib/antigravity/policy.rb', line 75 def initialize(&block) @rules = [] @confirm_handler = nil instance_eval(&block) if block_given? end |
Class Method Details
.allow_all ⇒ Object
85 86 87 |
# File 'lib/antigravity/policy.rb', line 85 def self.allow_all new { allow_all } end |
.auto ⇒ Object
🔮 Auto — reads RAILS_ENV, RACK_ENV, or ANTIGRAVITY_ENV and picks a preset. Falls back to :default if unrecognized or unset.
170 171 172 173 174 |
# File 'lib/antigravity/policy.rb', line 170 def self.auto env = ENV['ANTIGRAVITY_ENV'] || ENV['RAILS_ENV'] || ENV['RACK_ENV'] preset_name = ENV_MAP.fetch(env.to_s.downcase, :default) send(preset_name) end |
.cautious ⇒ Object
🔒 Cautious — read-only free, confirm everything else, hard-deny destructive. Best for: untrusted environments, production agents. NOTE: cat/head/tail/ls NOT in safe list — they can bypass view_file deny rules.
115 116 117 118 119 120 121 122 123 124 125 126 127 128 |
# File 'lib/antigravity/policy.rb', line 115 def self.cautious define do deny_all READONLY_TOOLS.each { |t| allow t } allow :run_command, when: cmd(*SAFE_CMDS, *SAFE_GIT_CMDS) deny :run_command, when: cmd(*CATASTROPHIC_CMDS) deny :run_command, when: cmd(*RISKY_CMDS) deny :run_command, when: cmd(*DESTRUCTIVE_GIT_CMDS) WRITE_TOOLS.each { |t| confirm t } # 📂 Sandbox dirs: always writable, even in production WRITE_TOOLS.each { |t| allow t, when: path(*SANDBOX_DIRS) } confirm :run_command end end |
.default ⇒ Object
⚖️ Default — balanced: allow reads + writes, confirm dangerous shell, protect sensitive files. Best for: day-to-day development, pair programming with an agent.
132 133 134 135 136 137 138 139 140 141 142 143 |
# File 'lib/antigravity/policy.rb', line 132 def self.default define do deny_all READONLY_TOOLS.each { |t| allow t } WRITE_TOOLS.each { |t| allow t } WRITE_TOOLS.each { |t| confirm t, when: path(*SENSITIVE_FILES) } allow :run_command deny :run_command, when: cmd(*CATASTROPHIC_CMDS) confirm :run_command, when: cmd(*RISKY_CMDS) confirm :run_command, when: cmd(*DESTRUCTIVE_GIT_CMDS) end end |
.define(&block) ⇒ Object
81 82 83 |
# File 'lib/antigravity/policy.rb', line 81 def self.define(&block) new(&block) end |
.deny_all ⇒ Object
89 90 91 |
# File 'lib/antigravity/policy.rb', line 89 def self.deny_all new { deny_all } end |
.preset(name) ⇒ Policy
Resolve a preset by name (symbol).
100 101 102 103 104 105 106 107 108 109 110 |
# File 'lib/antigravity/policy.rb', line 100 def self.preset(name) case name.to_sym when :cautious then cautious when :default then default when :turbo then turbo when :test then test when :auto then auto else raise ArgumentError, "Unknown preset :#{name}. Choose from: #{PRESET_NAMES.map { |n| ":#{n}" }.join(', ')}" end end |
.test ⇒ Object
🧪 Test — permissive for test runners, but sandboxed. Best for: CI, test suites, RAILS_ENV=test.
158 159 160 161 162 163 164 165 166 |
# File 'lib/antigravity/policy.rb', line 158 def self.test define do allow_all deny :run_command, when: cmd(*CATASTROPHIC_CMDS) deny :run_command, when: cmd(*DESTRUCTIVE_GIT_CMDS) confirm :run_command, when: cmd(*RISKY_CMDS) WRITE_TOOLS.each { |t| confirm t, when: path(*SENSITIVE_FILES) } end end |
.turbo ⇒ Object
🚀 Turbo — wide open with seatbelts: allow everything, only hard-deny catastrophic. Best for: trusted dev environments, rapid prototyping.
147 148 149 150 151 152 153 154 |
# File 'lib/antigravity/policy.rb', line 147 def self.turbo define do allow_all deny :run_command, when: cmd(*CATASTROPHIC_CMDS) confirm :run_command, when: cmd(*DESTRUCTIVE_GIT_CMDS) WRITE_TOOLS.each { |t| confirm t, when: path(*SENSITIVE_FILES) } end end |
Instance Method Details
#allow(tool_name = nil, **kwargs) ⇒ Object
DSL methods
180 181 182 |
# File 'lib/antigravity/policy.rb', line 180 def allow(tool_name = nil, **kwargs) @rules << Rule.new(:allow, tool_name, condition: kwargs[:when]) end |
#allow_all ⇒ Object
192 193 194 |
# File 'lib/antigravity/policy.rb', line 192 def allow_all allow(nil) end |
#args_match(**matchers) ⇒ Object
234 235 236 237 238 239 240 241 242 |
# File 'lib/antigravity/policy.rb', line 234 def args_match(**matchers) ->(ctx) do args = ctx[:args] matchers.any? do |k, v| val = args[k.to_sym] || args[k.to_s] val && v.match?(val.to_s) end end end |
#cmd(*patterns) ⇒ Object
Predicate helpers
208 209 210 211 212 213 214 215 216 217 |
# File 'lib/antigravity/policy.rb', line 208 def cmd(*patterns) ->(ctx) do args = ctx[:args] cmd_arg = args[:command_line] || args['command_line'] || args[:CommandLine] || args['CommandLine'] return false unless cmd_arg cmd_arg = cmd_arg.to_s patterns.any? { |p| cmd_arg.include?(p.to_s) } end end |
#confirm(tool_name = nil, **kwargs, &block) ⇒ Object
188 189 190 |
# File 'lib/antigravity/policy.rb', line 188 def confirm(tool_name = nil, **kwargs, &block) @rules << Rule.new(:confirm, tool_name, condition: kwargs[:when], handler: block) end |
#deny(tool_name = nil, **kwargs) ⇒ Object
184 185 186 |
# File 'lib/antigravity/policy.rb', line 184 def deny(tool_name = nil, **kwargs) @rules << Rule.new(:deny, tool_name, condition: kwargs[:when]) end |
#deny_all ⇒ Object
196 197 198 |
# File 'lib/antigravity/policy.rb', line 196 def deny_all deny(nil) end |
#evaluate(tool_name, args = {}) ⇒ Object
Evaluation engine
248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 |
# File 'lib/antigravity/policy.rb', line 248 def evaluate(tool_name, args = {}) matching_rules = @rules.select { |r| r.matches?(tool_name, args) } best_rule = matching_rules.max_by(&:precedence) if best_rule if best_rule.action == :confirm handler = best_rule.handler || @confirm_handler if handler ctx = { name: tool_name, args: args } result = handler.call(ctx) { status: result ? :allow : :deny } else { status: :deny } end elsif best_rule.action == :deny { status: :deny, reason: "Denied by policy" } else { status: :allow } end else { status: :deny } # Default to deny if no rules match end end |
#on_confirm(&block) ⇒ Object
200 201 202 |
# File 'lib/antigravity/policy.rb', line 200 def on_confirm(&block) @confirm_handler = block end |
#path(*globs) ⇒ Object
219 220 221 222 223 224 225 226 227 228 229 230 231 232 |
# File 'lib/antigravity/policy.rb', line 219 def path(*globs) ->(ctx) do args = ctx[:args] path_arg = args[:path] || args['path'] || args[:file] || args['file'] || args[:target] || args['target'] || args[:file_path] || args['file_path'] || args[:target_file] || args['target_file'] return false unless path_arg path_arg = path_arg.to_s globs.any? { |g| File.fnmatch?(g.to_s, path_arg) } end end |