Class: Antigravity::Policy

Inherits:
Object
  • Object
show all
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_file for a path, cat can 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

Instance Method Summary collapse

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_allObject



85
86
87
# File 'lib/antigravity/policy.rb', line 85

def self.allow_all
  new { allow_all }
end

.autoObject

🔮 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

.cautiousObject

🔒 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

.defaultObject

⚖️ 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_allObject



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).

Parameters:

  • name (Symbol)

    :cautious, :default, :turbo, :test, or :auto

Returns:



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

.testObject

🧪 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

.turboObject

🚀 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_allObject



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_allObject



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