antigravity-sdk 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/antigravity/agent.rb +290 -22
- data/lib/antigravity/colors.rb +43 -0
- data/lib/antigravity/config.rb +12 -1
- data/lib/antigravity/connection/binary_fetcher.rb +163 -0
- data/lib/antigravity/connection/local_connection.rb +184 -0
- data/lib/antigravity/connection/websocket_client.rb +159 -0
- data/lib/antigravity/conversation.rb +335 -0
- data/lib/antigravity/emojis.rb +4 -0
- data/lib/antigravity/errors.rb +27 -0
- data/lib/antigravity/guards.rb +82 -22
- data/lib/antigravity/hooks.rb +12 -0
- data/lib/antigravity/lifecycle_logger.rb +145 -0
- data/lib/antigravity/message.rb +19 -5
- data/lib/antigravity/policy/constants.rb +154 -0
- data/lib/antigravity/policy.rb +272 -0
- data/lib/antigravity/protocol.rb +193 -0
- data/lib/antigravity/skill.rb +53 -5
- data/lib/antigravity/skill_resolver.rb +140 -0
- data/lib/antigravity/tool.rb +33 -6
- data/lib/antigravity/tool_runner.rb +59 -0
- data/lib/antigravity.rb +11 -0
- metadata +16 -4
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Antigravity
|
|
4
|
+
# Base error for all Antigravity SDK errors
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised when the localharness binary cannot be found
|
|
8
|
+
class HarnessNotFoundError < Error; end
|
|
9
|
+
|
|
10
|
+
# Raised when the stdio handshake with localharness fails
|
|
11
|
+
class HarnessHandshakeError < Error; end
|
|
12
|
+
|
|
13
|
+
# Raised when protobuf encoding/decoding fails
|
|
14
|
+
class ProtocolError < Error; end
|
|
15
|
+
|
|
16
|
+
# Raised when a tool callback fails
|
|
17
|
+
class ToolError < Error; end
|
|
18
|
+
|
|
19
|
+
# Raised when trying to execute an unregistered tool
|
|
20
|
+
class ToolNotFoundError < ToolError; end
|
|
21
|
+
|
|
22
|
+
# Raised when WebSocket connection fails
|
|
23
|
+
class ConnectionError < Error; end
|
|
24
|
+
|
|
25
|
+
# Raised when required configuration (e.g., GEMINI_API_KEY) is missing
|
|
26
|
+
class ConfigError < Error; end
|
|
27
|
+
end
|
data/lib/antigravity/guards.rb
CHANGED
|
@@ -1,31 +1,48 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "json"
|
|
4
4
|
require "fileutils"
|
|
5
5
|
|
|
6
6
|
module Antigravity
|
|
7
7
|
module Guards
|
|
8
|
-
#
|
|
8
|
+
# Dual-output logger guard:
|
|
9
|
+
# 1. JSONL (log/antigravity.jsonl) — structured, machine-parseable, full data
|
|
10
|
+
# 2. Compact log (log/antigravity.log) — human-readable one-liners with byte sizes
|
|
11
|
+
# Falls back to Rails.logger for both if available.
|
|
9
12
|
class AgentLogger
|
|
10
|
-
attr_reader :
|
|
13
|
+
attr_reader :target_description
|
|
11
14
|
|
|
12
|
-
def initialize(log_target = nil, level:
|
|
13
|
-
|
|
15
|
+
def initialize(log_target = nil, level: :info, silent_notice: false)
|
|
16
|
+
resolved = resolve_log_target(log_target)
|
|
14
17
|
|
|
15
|
-
if
|
|
16
|
-
dir = File.dirname(
|
|
18
|
+
if resolved.is_a?(String)
|
|
19
|
+
dir = File.dirname(resolved)
|
|
17
20
|
FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@
|
|
21
|
+
|
|
22
|
+
# Fat JSONL log
|
|
23
|
+
@jsonl = File.open(resolved, 'a')
|
|
24
|
+
@jsonl.sync = true
|
|
25
|
+
|
|
26
|
+
# Skinny compact log (same dir, .log extension)
|
|
27
|
+
compact_path = resolved.sub(/\.jsonl$/, '.log')
|
|
28
|
+
@compact = File.open(compact_path, 'a')
|
|
29
|
+
@compact.sync = true
|
|
30
|
+
|
|
31
|
+
@rails_logger = nil
|
|
32
|
+
@target_description = resolved
|
|
33
|
+
elsif resolved.respond_to?(:info)
|
|
34
|
+
@jsonl = nil
|
|
35
|
+
@compact = nil
|
|
36
|
+
@rails_logger = resolved
|
|
22
37
|
@target_description = "Rails.logger"
|
|
23
38
|
else
|
|
24
|
-
@
|
|
39
|
+
@jsonl = $stdout
|
|
40
|
+
@compact = nil
|
|
41
|
+
@rails_logger = nil
|
|
25
42
|
@target_description = "$stdout"
|
|
26
43
|
end
|
|
27
44
|
|
|
28
|
-
@
|
|
45
|
+
@level = level
|
|
29
46
|
|
|
30
47
|
unless silent_notice
|
|
31
48
|
puts "#{Antigravity.emoji(:logger)} Logging to #{@target_description}"
|
|
@@ -33,25 +50,49 @@ module Antigravity
|
|
|
33
50
|
end
|
|
34
51
|
|
|
35
52
|
def before_prompt(prompt_text)
|
|
36
|
-
|
|
53
|
+
size = prompt_text.to_s.bytesize
|
|
54
|
+
log_jsonl('prompt', { user_input: prompt_text })
|
|
55
|
+
log_compact("#{Antigravity.emoji(:prompt)} PROMPT #{size}B | #{prompt_text.to_s[0, 80]}")
|
|
37
56
|
end
|
|
38
57
|
|
|
39
58
|
def after_response(response)
|
|
40
|
-
|
|
59
|
+
content = response.content&.strip || ''
|
|
60
|
+
log_jsonl('response', {
|
|
61
|
+
model: response.model_id,
|
|
62
|
+
content: content,
|
|
63
|
+
tokens: response.usage[:total_token_count],
|
|
64
|
+
tool_calls: response.tool_calls_count,
|
|
65
|
+
steps: response.steps&.length
|
|
66
|
+
})
|
|
67
|
+
log_compact("#{Antigravity.emoji(:response)} RESPONSE #{content.bytesize}B | " \
|
|
68
|
+
"tokens=#{response.usage[:total_token_count]} " \
|
|
69
|
+
"tools=#{response.tool_calls_count} " \
|
|
70
|
+
"steps=#{response.steps&.length} " \
|
|
71
|
+
"model=#{response.model_id}")
|
|
41
72
|
end
|
|
42
73
|
|
|
43
74
|
def before_tool_call(tool_name, params)
|
|
44
|
-
|
|
75
|
+
params_size = params.to_s.bytesize
|
|
76
|
+
log_jsonl('tool_call', { tool: tool_name, params: params })
|
|
77
|
+
log_compact("#{Antigravity.emoji(:tool)} TOOL_CALL #{tool_name} params=#{params_size}B")
|
|
45
78
|
end
|
|
46
79
|
|
|
47
80
|
def after_tool_call(tool_name, params, result)
|
|
48
|
-
|
|
49
|
-
|
|
81
|
+
blocked = result.to_s.include?("TOOL BLOCKED")
|
|
82
|
+
result_size = result.to_s.bytesize
|
|
83
|
+
log_jsonl('tool_result', {
|
|
84
|
+
tool: tool_name,
|
|
85
|
+
result: result.to_s[0, 500],
|
|
86
|
+
blocked: blocked
|
|
87
|
+
})
|
|
88
|
+
status = blocked ? 'BLOCKED' : 'OK'
|
|
89
|
+
log_compact("#{blocked ? Antigravity.emoji(:tool_blocked) : Antigravity.emoji(:tool_result)} TOOL_RESULT #{tool_name} #{status} result=#{result_size}B")
|
|
50
90
|
result
|
|
51
91
|
end
|
|
52
92
|
|
|
53
93
|
def sidecar_event(event_type, payload)
|
|
54
|
-
|
|
94
|
+
log_jsonl('sidecar', { type: event_type.to_s, payload: payload })
|
|
95
|
+
log_compact("#{Antigravity.emoji(:sidecar)} SIDECAR :#{event_type}")
|
|
55
96
|
end
|
|
56
97
|
|
|
57
98
|
def attach_to(agent)
|
|
@@ -64,17 +105,36 @@ module Antigravity
|
|
|
64
105
|
|
|
65
106
|
private
|
|
66
107
|
|
|
108
|
+
def ts
|
|
109
|
+
Time.now.utc.strftime('%Y-%m-%dT%H:%M:%S.%3NZ')
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def log_jsonl(event, data)
|
|
113
|
+
if @rails_logger
|
|
114
|
+
@rails_logger.info("[Antigravity] #{event}: #{data.inspect}")
|
|
115
|
+
elsif @jsonl
|
|
116
|
+
entry = { ts: ts, event: event, pid: Process.pid }.merge(data.compact)
|
|
117
|
+
@jsonl.puts(JSON.generate(entry))
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def log_compact(line)
|
|
122
|
+
if @compact
|
|
123
|
+
@compact.puts("#{ts} #{line}")
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
67
127
|
def resolve_log_target(target)
|
|
68
128
|
return target if target
|
|
69
129
|
|
|
70
130
|
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
71
131
|
Rails.logger
|
|
72
132
|
elsif ENV["RAILS_ENV"] && !ENV["RAILS_ENV"].empty?
|
|
73
|
-
"log/#{ENV['RAILS_ENV']}.
|
|
133
|
+
"log/#{ENV['RAILS_ENV']}.jsonl"
|
|
74
134
|
elsif ENV["RACK_ENV"] && !ENV["RACK_ENV"].empty?
|
|
75
|
-
"log/#{ENV['RACK_ENV']}.
|
|
135
|
+
"log/#{ENV['RACK_ENV']}.jsonl"
|
|
76
136
|
else
|
|
77
|
-
"log/antigravity.
|
|
137
|
+
"log/antigravity.jsonl"
|
|
78
138
|
end
|
|
79
139
|
end
|
|
80
140
|
end
|
data/lib/antigravity/hooks.rb
CHANGED
|
@@ -9,6 +9,7 @@ module Antigravity
|
|
|
9
9
|
@post_response_hooks = []
|
|
10
10
|
@pre_tool_hooks = []
|
|
11
11
|
@post_tool_hooks = []
|
|
12
|
+
@listeners = Hash.new { |h, k| h[k] = [] }
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def before_prompt(&block)
|
|
@@ -29,6 +30,17 @@ module Antigravity
|
|
|
29
30
|
@post_tool_hooks << block if block_given?
|
|
30
31
|
end
|
|
31
32
|
|
|
33
|
+
# Generic event system — subscribe to any named event.
|
|
34
|
+
# Usage: hooks.on(:ws_message) { |msg| puts msg.keys }
|
|
35
|
+
def on(event, &block)
|
|
36
|
+
@listeners[event.to_sym] << block if block_given?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Emit a named event to all subscribers.
|
|
40
|
+
def emit(event, *args)
|
|
41
|
+
@listeners[event.to_sym].each { |cb| cb.call(*args) }
|
|
42
|
+
end
|
|
43
|
+
|
|
32
44
|
def run_pre_prompt(prompt_text)
|
|
33
45
|
@pre_prompt_hooks.each { |hook| hook.call(prompt_text) }
|
|
34
46
|
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Auto-attachable lifecycle logger that prints colorful, compact status lines
|
|
4
|
+
# on every hook event. Inspired by Cloud Code's status bar.
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# agent = Antigravity::Agent.new
|
|
8
|
+
# Antigravity::LifecycleLogger.attach!(agent)
|
|
9
|
+
#
|
|
10
|
+
# Or auto-attach via env:
|
|
11
|
+
# ANTIGRAVITY_LIFECYCLE=1
|
|
12
|
+
# RAILS_ENV=test (auto-attaches in test/development)
|
|
13
|
+
#
|
|
14
|
+
module Antigravity
|
|
15
|
+
class LifecycleLogger
|
|
16
|
+
C = Antigravity::Colors
|
|
17
|
+
|
|
18
|
+
# Compact status string — the "Cloud Code status bar" equivalent.
|
|
19
|
+
# Example: "T3 | 1.2k tok | 4 tools | 2.3s"
|
|
20
|
+
def self.status_line(agent)
|
|
21
|
+
turns = agent.turn_count rescue 0
|
|
22
|
+
summary = agent.session_summary rescue {}
|
|
23
|
+
tokens = summary.dig(:tokens, :total) || 0
|
|
24
|
+
tok_str = tokens > 999 ? "#{(tokens / 1000.0).round(1)}k" : tokens.to_s
|
|
25
|
+
model = summary[:model] || agent.model || '?'
|
|
26
|
+
conv_id = (summary[:conversation_id] || '?')[0..7]
|
|
27
|
+
|
|
28
|
+
C.dim("T#{turns} | #{tok_str} tok | #{model} | #{conv_id}")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.attach!(agent, verbose: false)
|
|
32
|
+
new(verbose: verbose).attach(agent)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def initialize(verbose: false)
|
|
36
|
+
@verbose = verbose
|
|
37
|
+
@session_start_time = nil
|
|
38
|
+
@turn_start_time = nil
|
|
39
|
+
@turn_count = 0
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def attach(agent)
|
|
43
|
+
attach_session_hooks(agent)
|
|
44
|
+
attach_turn_hooks(agent)
|
|
45
|
+
attach_tool_hooks(agent)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def attach_session_hooks(agent)
|
|
51
|
+
logger = self
|
|
52
|
+
|
|
53
|
+
agent.hooks.on(:session_start) do |info|
|
|
54
|
+
logger.instance_variable_set(:@session_start_time, Process.clock_gettime(Process::CLOCK_MONOTONIC))
|
|
55
|
+
model = info[:model] || agent.model || '?'
|
|
56
|
+
conv_id = (info[:conversation_id] || '?')[0..11]
|
|
57
|
+
puts C.gray("🟢 #{C.dim("session_start")} | model=#{C.cyan(model)} | conv=#{C.cyan(conv_id)}")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
agent.hooks.on(:session_end) do |info|
|
|
61
|
+
elapsed = if logger.instance_variable_get(:@session_start_time)
|
|
62
|
+
(Process.clock_gettime(Process::CLOCK_MONOTONIC) - logger.instance_variable_get(:@session_start_time)).round(1)
|
|
63
|
+
else
|
|
64
|
+
'?'
|
|
65
|
+
end
|
|
66
|
+
turns = info[:turn_count] || agent.turn_count rescue 0
|
|
67
|
+
summary = agent.session_summary rescue {}
|
|
68
|
+
tokens = summary.dig(:tokens, :total) || 0
|
|
69
|
+
tok_str = tokens > 999 ? "#{(tokens / 1000.0).round(1)}k" : tokens.to_s
|
|
70
|
+
puts C.gray("🔴 #{C.dim("session_end")} | #{C.bold("#{turns} turns")} | #{tok_str} tok | #{elapsed}s")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def attach_turn_hooks(agent)
|
|
75
|
+
logger = self
|
|
76
|
+
|
|
77
|
+
agent.hooks.before_prompt do |text|
|
|
78
|
+
logger.instance_variable_set(:@turn_start_time, Process.clock_gettime(Process::CLOCK_MONOTONIC))
|
|
79
|
+
count = logger.instance_variable_get(:@turn_count) + 1
|
|
80
|
+
logger.instance_variable_set(:@turn_count, count)
|
|
81
|
+
preview = text.to_s[0..60].gsub("\n", ' ')
|
|
82
|
+
preview += '...' if text.to_s.length > 60
|
|
83
|
+
status = self.class.status_line(agent) rescue C.dim("T#{count}")
|
|
84
|
+
puts C.gray(" ➡️ #{C.dim("pre_turn")} T#{count} | #{C.yellow("\"#{preview}\"")} | #{status}")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
agent.hooks.after_response do |response|
|
|
88
|
+
elapsed = if logger.instance_variable_get(:@turn_start_time)
|
|
89
|
+
(Process.clock_gettime(Process::CLOCK_MONOTONIC) - logger.instance_variable_get(:@turn_start_time)).round(2)
|
|
90
|
+
else
|
|
91
|
+
'?'
|
|
92
|
+
end
|
|
93
|
+
content = response.respond_to?(:content) ? response.content.to_s : response.to_s
|
|
94
|
+
chars = content.length
|
|
95
|
+
lines = content.count("\n") + 1
|
|
96
|
+
preview = content[0..60]&.gsub("\n", ' ') || '(empty)'
|
|
97
|
+
preview += '...' if chars > 60
|
|
98
|
+
thinking_len = response.respond_to?(:thinking) ? (response.thinking&.length || 0) : 0
|
|
99
|
+
tool_count = response.respond_to?(:tool_calls_count) ? (response.tool_calls_count || 0) : 0
|
|
100
|
+
status = self.class.status_line(agent) rescue ''
|
|
101
|
+
|
|
102
|
+
parts = ["#{C.green("#{chars}ch")} #{C.dim("#{lines}L")}"]
|
|
103
|
+
parts << "#{C.magenta("#{thinking_len}ch")} think" if thinking_len > 0
|
|
104
|
+
parts << "#{C.cyan("#{tool_count}")} tools" if tool_count > 0
|
|
105
|
+
parts << "#{C.blue("#{elapsed}s")}"
|
|
106
|
+
|
|
107
|
+
puts C.gray(" ⬅️ #{C.dim("post_turn")} T#{logger.instance_variable_get(:@turn_count)} | #{parts.join(' | ')} | #{status}")
|
|
108
|
+
if logger.instance_variable_get(:@verbose)
|
|
109
|
+
puts C.dim(" \"#{preview}\"")
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def attach_tool_hooks(agent)
|
|
115
|
+
logger = self
|
|
116
|
+
|
|
117
|
+
agent.hooks.on(:tool_call) do |info|
|
|
118
|
+
name = info[:tool_name] || info[:name] || '?'
|
|
119
|
+
params_preview = (info[:params] || {}).keys.join(', ')
|
|
120
|
+
puts C.gray(" 🔧 #{C.dim("tool_call")} | #{C.cyan(name)}(#{C.dim(params_preview)})")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
agent.hooks.on(:tool_result) do |info|
|
|
124
|
+
name = info[:tool_name] || info[:name] || '?'
|
|
125
|
+
result_len = info[:result].to_s.length rescue 0
|
|
126
|
+
duration = info[:duration] ? "#{info[:duration].round(2)}s" : nil
|
|
127
|
+
parts = [C.cyan(name), "#{result_len}ch"]
|
|
128
|
+
parts << duration if duration
|
|
129
|
+
puts C.gray(" ✅ #{C.dim("tool_done")} | #{parts.join(' | ')}")
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
agent.hooks.on(:tool_blocked) do |info|
|
|
133
|
+
name = info[:tool] || '?'
|
|
134
|
+
reason = info[:reason] || 'policy'
|
|
135
|
+
puts C.red(" 🚫 #{C.dim("tool_deny")} | #{C.red(name)} — #{reason}")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
agent.hooks.on(:tool_error) do |info|
|
|
139
|
+
name = info[:tool_name] || info[:name] || '?'
|
|
140
|
+
error = info[:error] || info[:message] || '?'
|
|
141
|
+
puts C.red(" 💥 #{C.dim("tool_error")} | #{C.red(name)} — #{error.to_s[0..80]}")
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
data/lib/antigravity/message.rb
CHANGED
|
@@ -1,23 +1,37 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Antigravity
|
|
4
|
+
# Represents a response message from the agent.
|
|
5
|
+
# Mirrors Python SDK's ChatResponse with metadata.
|
|
4
6
|
class Message < Base
|
|
7
|
+
attr_accessor :role, :content, :thinking, :tool_calls, :model_id, :tokens,
|
|
8
|
+
:steps, :tool_calls_count, :usage, :delta
|
|
5
9
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
def initialize(role: :assistant, content: '', thinking: '', tool_calls: [],
|
|
11
|
+
model_id: nil, steps: [], tool_calls_count: 0, usage: nil,
|
|
12
|
+
delta: false)
|
|
9
13
|
@role = role
|
|
10
14
|
@content = content
|
|
11
15
|
@thinking = thinking
|
|
12
16
|
@tool_calls = tool_calls
|
|
13
17
|
@model_id = model_id
|
|
14
18
|
@tokens = { input: 0, output: 0 }
|
|
19
|
+
@steps = steps
|
|
20
|
+
@tool_calls_count = tool_calls_count
|
|
21
|
+
@usage = usage || { prompt_token_count: 0, candidates_token_count: 0, total_token_count: 0 }
|
|
22
|
+
@delta = delta
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Is this a streaming delta (partial) or a complete response?
|
|
26
|
+
def delta?
|
|
27
|
+
@delta
|
|
15
28
|
end
|
|
16
29
|
end
|
|
17
30
|
|
|
31
|
+
# Backward-compatible alias for streaming chunks
|
|
18
32
|
class Chunk < Message
|
|
19
|
-
def initialize(
|
|
20
|
-
super(
|
|
33
|
+
def initialize(**kwargs)
|
|
34
|
+
super(**kwargs, delta: true)
|
|
21
35
|
end
|
|
22
36
|
end
|
|
23
37
|
end
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# ==========================================================================
|
|
4
|
+
# Policy Constants — curated lists of commands, tools, and file patterns.
|
|
5
|
+
#
|
|
6
|
+
# 🛠️ MAINTAINER NOTES:
|
|
7
|
+
# - One entry per line for easy review in PRs.
|
|
8
|
+
# - Add new entries alphabetically within each group.
|
|
9
|
+
# - Multi-word commands use explicit quotes (no backslash escapes!).
|
|
10
|
+
# - Run `bundle exec rspec spec/antigravity/policy_spec.rb` after edits.
|
|
11
|
+
# ==========================================================================
|
|
12
|
+
|
|
13
|
+
module Antigravity
|
|
14
|
+
class Policy
|
|
15
|
+
|
|
16
|
+
# ------------------------------------------------------------------
|
|
17
|
+
# 💀 Catastrophic commands — hard-denied in ALL presets, no exceptions.
|
|
18
|
+
# ------------------------------------------------------------------
|
|
19
|
+
CATASTROPHIC_CMDS = [
|
|
20
|
+
'dd if=/dev/urandom',
|
|
21
|
+
'dd if=/dev/zero',
|
|
22
|
+
'> /dev/sd',
|
|
23
|
+
'halt',
|
|
24
|
+
'mkfs',
|
|
25
|
+
'reboot',
|
|
26
|
+
'rm -rf /*',
|
|
27
|
+
'rm -rf /',
|
|
28
|
+
'rm -rf ~',
|
|
29
|
+
'shutdown',
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
# ------------------------------------------------------------------
|
|
33
|
+
# ⚠️ Risky commands — confirmed in :default, hard-denied in :cautious.
|
|
34
|
+
# Single-word or short patterns that match substring in command_line.
|
|
35
|
+
# ------------------------------------------------------------------
|
|
36
|
+
RISKY_CMDS = [
|
|
37
|
+
'chmod -R 777',
|
|
38
|
+
'chown -R',
|
|
39
|
+
'kill -9',
|
|
40
|
+
'killall',
|
|
41
|
+
'pkill',
|
|
42
|
+
'rm',
|
|
43
|
+
'xargs',
|
|
44
|
+
].freeze
|
|
45
|
+
|
|
46
|
+
# ------------------------------------------------------------------
|
|
47
|
+
# 🔥 Destructive git — nuke local changes, rewrite history.
|
|
48
|
+
# Confirmed in :default/:turbo, hard-denied in :cautious/:test.
|
|
49
|
+
# ------------------------------------------------------------------
|
|
50
|
+
DESTRUCTIVE_GIT_CMDS = [
|
|
51
|
+
'git checkout .',
|
|
52
|
+
'git checkout -- .',
|
|
53
|
+
'git clean -fd',
|
|
54
|
+
'git clean -fdx',
|
|
55
|
+
'git push --force',
|
|
56
|
+
'git push -f',
|
|
57
|
+
'git reset --hard',
|
|
58
|
+
'git stash drop',
|
|
59
|
+
].freeze
|
|
60
|
+
|
|
61
|
+
# ------------------------------------------------------------------
|
|
62
|
+
# ✅ Safe read-only shell commands — allowed even in :cautious.
|
|
63
|
+
# ------------------------------------------------------------------
|
|
64
|
+
SAFE_CMDS = %i[
|
|
65
|
+
cd
|
|
66
|
+
date
|
|
67
|
+
echo
|
|
68
|
+
hostname
|
|
69
|
+
ls
|
|
70
|
+
md5
|
|
71
|
+
md5sum
|
|
72
|
+
pwd
|
|
73
|
+
uname
|
|
74
|
+
wc
|
|
75
|
+
which
|
|
76
|
+
whoami
|
|
77
|
+
].freeze
|
|
78
|
+
|
|
79
|
+
# ⚠️ File-reading shell commands — can bypass view_file deny rules!
|
|
80
|
+
# Allowed in :default/:turbo/:test, but NOT in :cautious.
|
|
81
|
+
# If you deny `view_file` for a path, `cat` can circumvent it.
|
|
82
|
+
READ_CMDS = %i[
|
|
83
|
+
cat
|
|
84
|
+
head
|
|
85
|
+
strings
|
|
86
|
+
tail
|
|
87
|
+
].freeze
|
|
88
|
+
|
|
89
|
+
# Safe git subcommands (read-only, no mutations)
|
|
90
|
+
SAFE_GIT_CMDS = [
|
|
91
|
+
'git branch',
|
|
92
|
+
'git diff',
|
|
93
|
+
'git log',
|
|
94
|
+
'git remote',
|
|
95
|
+
'git status',
|
|
96
|
+
].freeze
|
|
97
|
+
|
|
98
|
+
# ------------------------------------------------------------------
|
|
99
|
+
# 🔐 Sensitive file globs — writes to these require confirmation.
|
|
100
|
+
# ------------------------------------------------------------------
|
|
101
|
+
SENSITIVE_FILES = [
|
|
102
|
+
'.env',
|
|
103
|
+
'.env.*',
|
|
104
|
+
'*.key',
|
|
105
|
+
'*.pem',
|
|
106
|
+
'*.secret',
|
|
107
|
+
'id_rsa*',
|
|
108
|
+
].freeze
|
|
109
|
+
|
|
110
|
+
# ------------------------------------------------------------------
|
|
111
|
+
# 📂 Sandbox directories — always writable, even in production.
|
|
112
|
+
# Throwaway / output dirs where agents can freely write.
|
|
113
|
+
# ------------------------------------------------------------------
|
|
114
|
+
SANDBOX_DIRS = [
|
|
115
|
+
'out/*',
|
|
116
|
+
'scratch/*',
|
|
117
|
+
].freeze
|
|
118
|
+
|
|
119
|
+
# ------------------------------------------------------------------
|
|
120
|
+
# 🔧 Tool classifications
|
|
121
|
+
# ------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
# Read-only harness tools (always safe)
|
|
124
|
+
READONLY_TOOLS = %i[
|
|
125
|
+
find
|
|
126
|
+
grep_search
|
|
127
|
+
list_dir
|
|
128
|
+
read_url_content
|
|
129
|
+
search_web
|
|
130
|
+
view_file
|
|
131
|
+
].freeze
|
|
132
|
+
|
|
133
|
+
# Write harness tools
|
|
134
|
+
WRITE_TOOLS = %i[
|
|
135
|
+
file_edit
|
|
136
|
+
write_to_file
|
|
137
|
+
].freeze
|
|
138
|
+
|
|
139
|
+
# ------------------------------------------------------------------
|
|
140
|
+
# 🗺️ Environment → preset mapping (for Policy.auto)
|
|
141
|
+
# ------------------------------------------------------------------
|
|
142
|
+
PRESET_NAMES = %i[auto cautious default test turbo].freeze
|
|
143
|
+
|
|
144
|
+
ENV_MAP = {
|
|
145
|
+
'dev' => :turbo,
|
|
146
|
+
'development' => :turbo,
|
|
147
|
+
'prod' => :cautious,
|
|
148
|
+
'production' => :cautious,
|
|
149
|
+
'staging' => :default,
|
|
150
|
+
'test' => :test,
|
|
151
|
+
}.freeze
|
|
152
|
+
|
|
153
|
+
end
|
|
154
|
+
end
|