antigravity-sdk 0.4.2 → 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 +57 -1
- data/lib/antigravity/colors.rb +43 -0
- data/lib/antigravity/conversation.rb +36 -3
- data/lib/antigravity/lifecycle_logger.rb +145 -0
- data/lib/antigravity/policy/constants.rb +154 -0
- data/lib/antigravity/policy.rb +272 -0
- data/lib/antigravity.rb +3 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: effdabdb0588788c4b168749a2578e2e1f07aa877a54bfd80fe1969d42f497d2
|
|
4
|
+
data.tar.gz: 3debe5e4295f8c2e2bcd4c37ec7ea52ea11a77d034370320d4064b52aa08cc29
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4123cbc798ddbfaa82ea0f854e89adb122491d9a99b52f501c15350401e660109be3ca778b6142720f0259256992dbbb73d4d2371d8bdefcda266bd4d98591cf
|
|
7
|
+
data.tar.gz: d80349e13c564c74bb81c5c8ba14887f39af36507d03b3c6a5883ed3a885359570210d10b9fc1f8cc3e8a0c193408de176f3316129a58e1d6303352c5527ed01
|
data/lib/antigravity/agent.rb
CHANGED
|
@@ -8,13 +8,14 @@ module Antigravity
|
|
|
8
8
|
:workspace, :connection, :conversation
|
|
9
9
|
|
|
10
10
|
def initialize(model: nil, system_instruction: nil, tools: [],
|
|
11
|
-
skills: [], workspace: nil, auto_logger: true, log_file: nil, &block)
|
|
11
|
+
skills: [], policies: [], policy: nil, workspace: nil, auto_logger: true, log_file: nil, &block)
|
|
12
12
|
@model = model || Antigravity.config.default_model
|
|
13
13
|
@api_key = Antigravity.config.api_key
|
|
14
14
|
@system_instruction = system_instruction
|
|
15
15
|
@workspace = workspace ? File.expand_path(workspace) : nil
|
|
16
16
|
@tools = tools.dup
|
|
17
17
|
@skills = []
|
|
18
|
+
@policies = []
|
|
18
19
|
@sidecars = []
|
|
19
20
|
@hooks = Hooks.new
|
|
20
21
|
@client = Client.new
|
|
@@ -30,6 +31,15 @@ module Antigravity
|
|
|
30
31
|
# Load skills provided at construction (local paths or GitHub URLs)
|
|
31
32
|
add_skills(skills) unless Array(skills).empty?
|
|
32
33
|
|
|
34
|
+
# Resolve policy: sugar (symbol → preset, Policy object → use directly)
|
|
35
|
+
if policy
|
|
36
|
+
resolved = policy.is_a?(Symbol) ? Policy.preset(policy) : policy
|
|
37
|
+
enforce(resolved)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Load policies
|
|
41
|
+
policies.each { |p| enforce(p) }
|
|
42
|
+
|
|
33
43
|
# Automagic Logger attachment unless disabled via ENV["ANTIGRAVITY_LOGGER"]=false or auto_logger: false
|
|
34
44
|
if auto_logger && logger_enabled?
|
|
35
45
|
attach_logger(log_file)
|
|
@@ -38,6 +48,13 @@ module Antigravity
|
|
|
38
48
|
yield(self) if block_given?
|
|
39
49
|
end
|
|
40
50
|
|
|
51
|
+
def enforce(policy)
|
|
52
|
+
@policies << policy
|
|
53
|
+
before_tool_call do |tool_name, args|
|
|
54
|
+
policy.evaluate(tool_name, args)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
41
58
|
# --- Class Methods ---
|
|
42
59
|
|
|
43
60
|
# Block form: opens connection, yields agent, auto-closes.
|
|
@@ -56,6 +73,12 @@ module Antigravity
|
|
|
56
73
|
def connect!
|
|
57
74
|
return self if @connected
|
|
58
75
|
|
|
76
|
+
# Auto-attach lifecycle logger if enabled via env
|
|
77
|
+
if lifecycle_logger_enabled? && !@lifecycle_attached
|
|
78
|
+
LifecycleLogger.attach!(self, verbose: ENV['ANTIGRAVITY_LIFECYCLE_VERBOSE'] == '1')
|
|
79
|
+
@lifecycle_attached = true
|
|
80
|
+
end
|
|
81
|
+
|
|
59
82
|
@connection = Connection::LocalConnection.new
|
|
60
83
|
@connection.connect!
|
|
61
84
|
|
|
@@ -68,6 +91,16 @@ module Antigravity
|
|
|
68
91
|
harness_config = build_harness_config
|
|
69
92
|
@conversation.initialize_session!(harness_config: harness_config)
|
|
70
93
|
@connected = true
|
|
94
|
+
|
|
95
|
+
# Emit session_start hook
|
|
96
|
+
hooks.emit(:session_start, {
|
|
97
|
+
model: @model,
|
|
98
|
+
conversation_id: conversation_id,
|
|
99
|
+
workspace: @workspace,
|
|
100
|
+
skills_count: @skills.length,
|
|
101
|
+
tools_count: @tools.length,
|
|
102
|
+
})
|
|
103
|
+
|
|
71
104
|
self
|
|
72
105
|
end
|
|
73
106
|
|
|
@@ -76,6 +109,14 @@ module Antigravity
|
|
|
76
109
|
end
|
|
77
110
|
|
|
78
111
|
def close!
|
|
112
|
+
# Emit session_end hook before teardown
|
|
113
|
+
if @connected
|
|
114
|
+
hooks.emit(:session_end, {
|
|
115
|
+
turn_count: turn_count,
|
|
116
|
+
conversation_id: conversation_id,
|
|
117
|
+
})
|
|
118
|
+
end
|
|
119
|
+
|
|
79
120
|
@connected = false
|
|
80
121
|
@connection&.disconnect!
|
|
81
122
|
@connection = nil
|
|
@@ -85,6 +126,7 @@ module Antigravity
|
|
|
85
126
|
# --- Chat ---
|
|
86
127
|
|
|
87
128
|
def prompt(message, timeout: Antigravity.config.timeout_llm, &block)
|
|
129
|
+
connect! unless @connected
|
|
88
130
|
emit_sidecar_event(:prompt_started, prompt: message)
|
|
89
131
|
hooks.run_pre_prompt(message)
|
|
90
132
|
|
|
@@ -223,6 +265,16 @@ module Antigravity
|
|
|
223
265
|
true
|
|
224
266
|
end
|
|
225
267
|
|
|
268
|
+
def lifecycle_logger_enabled?
|
|
269
|
+
# Explicit opt-in
|
|
270
|
+
return true if ENV['ANTIGRAVITY_LIFECYCLE'] == '1'
|
|
271
|
+
# Rails/Rack test or development
|
|
272
|
+
return true if %w[test development].include?(ENV['RAILS_ENV']&.downcase)
|
|
273
|
+
return true if %w[test development].include?(ENV['RACK_ENV']&.downcase)
|
|
274
|
+
# Explicit opt-out
|
|
275
|
+
false
|
|
276
|
+
end
|
|
277
|
+
|
|
226
278
|
def build_harness_config
|
|
227
279
|
api_key = ENV.fetch('GEMINI_API_KEY') {
|
|
228
280
|
raise ConfigError, 'GEMINI_API_KEY environment variable is required'
|
|
@@ -308,5 +360,9 @@ module Antigravity
|
|
|
308
360
|
@skills << skill
|
|
309
361
|
skill
|
|
310
362
|
end
|
|
363
|
+
|
|
364
|
+
def lifecycle_logger_enabled?
|
|
365
|
+
ENV['ANTIGRAVITY_LIFECYCLE'] == '1'
|
|
366
|
+
end
|
|
311
367
|
end
|
|
312
368
|
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Lightweight ANSI color helpers for terminal output.
|
|
4
|
+
# No external dependencies. Safe for piped/non-TTY output.
|
|
5
|
+
module Antigravity
|
|
6
|
+
module Colors
|
|
7
|
+
CODES = {
|
|
8
|
+
reset: "\e[0m",
|
|
9
|
+
bold: "\e[1m",
|
|
10
|
+
dim: "\e[2m",
|
|
11
|
+
italic: "\e[3m",
|
|
12
|
+
# Foreground
|
|
13
|
+
gray: "\e[90m",
|
|
14
|
+
red: "\e[31m",
|
|
15
|
+
green: "\e[32m",
|
|
16
|
+
yellow: "\e[33m",
|
|
17
|
+
blue: "\e[34m",
|
|
18
|
+
magenta: "\e[35m",
|
|
19
|
+
cyan: "\e[36m",
|
|
20
|
+
white: "\e[37m",
|
|
21
|
+
# Bright
|
|
22
|
+
bright_green: "\e[92m",
|
|
23
|
+
bright_yellow: "\e[93m",
|
|
24
|
+
bright_cyan: "\e[96m",
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
def self.colorize(text, *styles)
|
|
28
|
+
return text.to_s unless $stdout.tty?
|
|
29
|
+
prefix = styles.map { |s| CODES[s] || "" }.join
|
|
30
|
+
"#{prefix}#{text}#{CODES[:reset]}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.gray(text) = colorize(text, :gray)
|
|
34
|
+
def self.dim(text) = colorize(text, :dim)
|
|
35
|
+
def self.green(text) = colorize(text, :green)
|
|
36
|
+
def self.yellow(text) = colorize(text, :yellow)
|
|
37
|
+
def self.red(text) = colorize(text, :red)
|
|
38
|
+
def self.cyan(text) = colorize(text, :cyan)
|
|
39
|
+
def self.blue(text) = colorize(text, :blue)
|
|
40
|
+
def self.magenta(text) = colorize(text, :magenta)
|
|
41
|
+
def self.bold(text) = colorize(text, :bold)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -59,6 +59,11 @@ module Antigravity
|
|
|
59
59
|
@turn_count += 1
|
|
60
60
|
@last_turn_usage = empty_usage
|
|
61
61
|
|
|
62
|
+
# GHI #18: Drain any stale messages from the WebSocket buffer before sending
|
|
63
|
+
# a new prompt. This prevents leftover FULLY_IDLE from previous turns or init
|
|
64
|
+
# from being consumed by collect_response.
|
|
65
|
+
drain_stale_messages
|
|
66
|
+
|
|
62
67
|
# Send user input (protobuf InputEvent with user_input string field)
|
|
63
68
|
input_event = {
|
|
64
69
|
userInput: prompt
|
|
@@ -87,6 +92,20 @@ module Antigravity
|
|
|
87
92
|
|
|
88
93
|
private
|
|
89
94
|
|
|
95
|
+
# GHI #18: Non-blocking drain of stale WebSocket messages (FULLY_IDLE leftovers).
|
|
96
|
+
# During gaps between turns (e.g. voice transcription taking 5-10s), the harness
|
|
97
|
+
# may send trajectory updates that would confuse the next collect_response call.
|
|
98
|
+
def drain_stale_messages
|
|
99
|
+
drained = 0
|
|
100
|
+
loop do
|
|
101
|
+
msg = @ws.receive_json(timeout: 0.05, idle_timeout: 0.05) rescue nil
|
|
102
|
+
break unless msg
|
|
103
|
+
drained += 1
|
|
104
|
+
@hooks&.emit(:ws_message, { _debug: 'drained_stale_message', message_keys: msg.keys, drained_count: drained })
|
|
105
|
+
end
|
|
106
|
+
@hooks&.emit(:ws_message, { _debug: 'drain_complete', count: drained }) if drained > 0
|
|
107
|
+
end
|
|
108
|
+
|
|
90
109
|
def collect_response(timeout: Antigravity.config.timeout_llm, &block)
|
|
91
110
|
text_parts = []
|
|
92
111
|
thinking_parts = []
|
|
@@ -94,11 +113,14 @@ module Antigravity
|
|
|
94
113
|
tool_calls_count = 0
|
|
95
114
|
finished = false
|
|
96
115
|
finished_at = nil
|
|
116
|
+
seen_any_step = false # GHI #18: track if we've seen real work from this turn
|
|
117
|
+
turn_started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
97
118
|
|
|
98
119
|
@ws.each_message(timeout: timeout) do |msg|
|
|
99
120
|
@hooks&.emit(:ws_message, msg)
|
|
100
121
|
|
|
101
122
|
if (step = msg[:stepUpdate])
|
|
123
|
+
seen_any_step = true
|
|
102
124
|
step_record = parse_step(step)
|
|
103
125
|
steps << step_record
|
|
104
126
|
|
|
@@ -144,20 +166,31 @@ module Antigravity
|
|
|
144
166
|
# Top-level tool call (custom tools are sent as separate messages, not in stepUpdate)
|
|
145
167
|
if (tool_call = msg[:toolCall])
|
|
146
168
|
tool_calls_count += 1
|
|
169
|
+
seen_any_step = true
|
|
147
170
|
handle_tool_call(tool_call)
|
|
148
171
|
end
|
|
149
172
|
|
|
150
173
|
# Usage update
|
|
151
174
|
if (usage = msg[:usageUpdate])
|
|
152
175
|
update_usage(usage)
|
|
176
|
+
seen_any_step = true
|
|
153
177
|
end
|
|
154
178
|
|
|
155
179
|
# Trajectory state: STATE_FULLY_IDLE / STATE_CANCELLED = turn complete (authoritative signal from harness)
|
|
156
|
-
#
|
|
180
|
+
# GHI #18 FIX: Only honor FULLY_IDLE if we've seen at least one stepUpdate/toolCall/usageUpdate
|
|
181
|
+
# from this turn, OR if enough time has elapsed (1s) that this can't be a stale leftover.
|
|
182
|
+
# A stale FULLY_IDLE from a previous turn sitting in the WebSocket buffer was causing
|
|
183
|
+
# collect_response to return immediately with zero steps/text.
|
|
157
184
|
if (traj = msg[:trajectoryStateUpdate])
|
|
185
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - turn_started_at
|
|
158
186
|
if traj[:state].to_s =~ /FULLY_IDLE|CANCELLED/
|
|
159
|
-
|
|
160
|
-
|
|
187
|
+
if seen_any_step || elapsed > 1.0
|
|
188
|
+
finished = true
|
|
189
|
+
finished_at = Time.now
|
|
190
|
+
else
|
|
191
|
+
# Stale FULLY_IDLE — skip it (likely leftover from previous turn or init)
|
|
192
|
+
@hooks&.emit(:ws_message, { _debug: 'skipped_stale_fully_idle', elapsed: elapsed.round(3) })
|
|
193
|
+
end
|
|
161
194
|
end
|
|
162
195
|
end
|
|
163
196
|
|
|
@@ -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
|
|
@@ -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
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'policy/constants'
|
|
4
|
+
|
|
5
|
+
module Antigravity
|
|
6
|
+
# ==========================================================================
|
|
7
|
+
# Antigravity::Policy — Declarative tool-access control for agents.
|
|
8
|
+
#
|
|
9
|
+
# ⚠️ ORDER DOES NOT MATTER!
|
|
10
|
+
#
|
|
11
|
+
# The DSL is declarative, like SQL — not imperative like a script.
|
|
12
|
+
# Rules are evaluated by PRECEDENCE, not by insertion order.
|
|
13
|
+
# You can write `allow` before `deny` or vice versa — same result.
|
|
14
|
+
#
|
|
15
|
+
# Precedence (highest wins):
|
|
16
|
+
# 1. Tool specificity: specific tool > wildcard (nil)
|
|
17
|
+
# 2. Condition specificity: has `when:` > no `when:`
|
|
18
|
+
# 3. Restrictiveness: deny > confirm > allow
|
|
19
|
+
#
|
|
20
|
+
# Example — these two policies behave identically:
|
|
21
|
+
#
|
|
22
|
+
# Policy.define do Policy.define do
|
|
23
|
+
# allow :run_command deny :run_command, when: cmd('rm')
|
|
24
|
+
# deny :run_command, allow :run_command
|
|
25
|
+
# when: cmd('rm') end
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# In both cases, `rm` is denied (conditional deny beats unconditional
|
|
29
|
+
# allow), and everything else is allowed.
|
|
30
|
+
#
|
|
31
|
+
# See policy/constants.rb for curated command/file/tool lists.
|
|
32
|
+
# ==========================================================================
|
|
33
|
+
class Policy
|
|
34
|
+
|
|
35
|
+
# ------------------------------------------------------------------
|
|
36
|
+
# Rule — a single allow/deny/confirm entry in a policy.
|
|
37
|
+
# ------------------------------------------------------------------
|
|
38
|
+
class Rule
|
|
39
|
+
attr_reader :action, :tool_name, :condition, :handler
|
|
40
|
+
|
|
41
|
+
def initialize(action, tool_name = nil, condition: nil, handler: nil)
|
|
42
|
+
@action = action
|
|
43
|
+
@tool_name = tool_name
|
|
44
|
+
@condition = condition
|
|
45
|
+
@handler = handler
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def matches?(tool, args)
|
|
49
|
+
return false if @tool_name && @tool_name.to_sym != tool.to_sym
|
|
50
|
+
return false if @condition && !@condition.call(name: tool, args: args)
|
|
51
|
+
true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Precedence order (higher = wins):
|
|
55
|
+
# 1. Tool specificity: Specific tool > Wildcard (nil)
|
|
56
|
+
# 2. Condition specificity: Has predicate > No predicate
|
|
57
|
+
# 3. Action restrictiveness: Deny > Confirm > Allow
|
|
58
|
+
def precedence
|
|
59
|
+
specificity = @tool_name ? 1 : 0
|
|
60
|
+
condition_score = @condition ? 1 : 0
|
|
61
|
+
action_score = case @action
|
|
62
|
+
when :deny then 3
|
|
63
|
+
when :confirm then 2
|
|
64
|
+
when :allow then 1
|
|
65
|
+
else 0
|
|
66
|
+
end
|
|
67
|
+
[specificity, condition_score, action_score]
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# ------------------------------------------------------------------
|
|
72
|
+
# Constructor & factory methods
|
|
73
|
+
# ------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
def initialize(&block)
|
|
76
|
+
@rules = []
|
|
77
|
+
@confirm_handler = nil
|
|
78
|
+
instance_eval(&block) if block_given?
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.define(&block)
|
|
82
|
+
new(&block)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self.allow_all
|
|
86
|
+
new { allow_all }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def self.deny_all
|
|
90
|
+
new { deny_all }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# ------------------------------------------------------------------
|
|
94
|
+
# Built-in presets
|
|
95
|
+
# ------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
# Resolve a preset by name (symbol).
|
|
98
|
+
# @param name [Symbol] :cautious, :default, :turbo, :test, or :auto
|
|
99
|
+
# @return [Policy]
|
|
100
|
+
def self.preset(name)
|
|
101
|
+
case name.to_sym
|
|
102
|
+
when :cautious then cautious
|
|
103
|
+
when :default then default
|
|
104
|
+
when :turbo then turbo
|
|
105
|
+
when :test then test
|
|
106
|
+
when :auto then auto
|
|
107
|
+
else
|
|
108
|
+
raise ArgumentError, "Unknown preset :#{name}. Choose from: #{PRESET_NAMES.map { |n| ":#{n}" }.join(', ')}"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# 🔒 Cautious — read-only free, confirm everything else, hard-deny destructive.
|
|
113
|
+
# Best for: untrusted environments, production agents.
|
|
114
|
+
# NOTE: cat/head/tail/ls NOT in safe list — they can bypass view_file deny rules.
|
|
115
|
+
def self.cautious
|
|
116
|
+
define do
|
|
117
|
+
deny_all
|
|
118
|
+
READONLY_TOOLS.each { |t| allow t }
|
|
119
|
+
allow :run_command, when: cmd(*SAFE_CMDS, *SAFE_GIT_CMDS)
|
|
120
|
+
deny :run_command, when: cmd(*CATASTROPHIC_CMDS)
|
|
121
|
+
deny :run_command, when: cmd(*RISKY_CMDS)
|
|
122
|
+
deny :run_command, when: cmd(*DESTRUCTIVE_GIT_CMDS)
|
|
123
|
+
WRITE_TOOLS.each { |t| confirm t }
|
|
124
|
+
# 📂 Sandbox dirs: always writable, even in production
|
|
125
|
+
WRITE_TOOLS.each { |t| allow t, when: path(*SANDBOX_DIRS) }
|
|
126
|
+
confirm :run_command
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# ⚖️ Default — balanced: allow reads + writes, confirm dangerous shell, protect sensitive files.
|
|
131
|
+
# Best for: day-to-day development, pair programming with an agent.
|
|
132
|
+
def self.default
|
|
133
|
+
define do
|
|
134
|
+
deny_all
|
|
135
|
+
READONLY_TOOLS.each { |t| allow t }
|
|
136
|
+
WRITE_TOOLS.each { |t| allow t }
|
|
137
|
+
WRITE_TOOLS.each { |t| confirm t, when: path(*SENSITIVE_FILES) }
|
|
138
|
+
allow :run_command
|
|
139
|
+
deny :run_command, when: cmd(*CATASTROPHIC_CMDS)
|
|
140
|
+
confirm :run_command, when: cmd(*RISKY_CMDS)
|
|
141
|
+
confirm :run_command, when: cmd(*DESTRUCTIVE_GIT_CMDS)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# 🚀 Turbo — wide open with seatbelts: allow everything, only hard-deny catastrophic.
|
|
146
|
+
# Best for: trusted dev environments, rapid prototyping.
|
|
147
|
+
def self.turbo
|
|
148
|
+
define do
|
|
149
|
+
allow_all
|
|
150
|
+
deny :run_command, when: cmd(*CATASTROPHIC_CMDS)
|
|
151
|
+
confirm :run_command, when: cmd(*DESTRUCTIVE_GIT_CMDS)
|
|
152
|
+
WRITE_TOOLS.each { |t| confirm t, when: path(*SENSITIVE_FILES) }
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# 🧪 Test — permissive for test runners, but sandboxed.
|
|
157
|
+
# Best for: CI, test suites, RAILS_ENV=test.
|
|
158
|
+
def self.test
|
|
159
|
+
define do
|
|
160
|
+
allow_all
|
|
161
|
+
deny :run_command, when: cmd(*CATASTROPHIC_CMDS)
|
|
162
|
+
deny :run_command, when: cmd(*DESTRUCTIVE_GIT_CMDS)
|
|
163
|
+
confirm :run_command, when: cmd(*RISKY_CMDS)
|
|
164
|
+
WRITE_TOOLS.each { |t| confirm t, when: path(*SENSITIVE_FILES) }
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# 🔮 Auto — reads RAILS_ENV, RACK_ENV, or ANTIGRAVITY_ENV and picks a preset.
|
|
169
|
+
# Falls back to :default if unrecognized or unset.
|
|
170
|
+
def self.auto
|
|
171
|
+
env = ENV['ANTIGRAVITY_ENV'] || ENV['RAILS_ENV'] || ENV['RACK_ENV']
|
|
172
|
+
preset_name = ENV_MAP.fetch(env.to_s.downcase, :default)
|
|
173
|
+
send(preset_name)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# ------------------------------------------------------------------
|
|
177
|
+
# DSL methods
|
|
178
|
+
# ------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
def allow(tool_name = nil, **kwargs)
|
|
181
|
+
@rules << Rule.new(:allow, tool_name, condition: kwargs[:when])
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def deny(tool_name = nil, **kwargs)
|
|
185
|
+
@rules << Rule.new(:deny, tool_name, condition: kwargs[:when])
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def confirm(tool_name = nil, **kwargs, &block)
|
|
189
|
+
@rules << Rule.new(:confirm, tool_name, condition: kwargs[:when], handler: block)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def allow_all
|
|
193
|
+
allow(nil)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def deny_all
|
|
197
|
+
deny(nil)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def on_confirm(&block)
|
|
201
|
+
@confirm_handler = block
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# ------------------------------------------------------------------
|
|
205
|
+
# Predicate helpers
|
|
206
|
+
# ------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
def cmd(*patterns)
|
|
209
|
+
->(ctx) do
|
|
210
|
+
args = ctx[:args]
|
|
211
|
+
cmd_arg = args[:command_line] || args['command_line'] || args[:CommandLine] || args['CommandLine']
|
|
212
|
+
return false unless cmd_arg
|
|
213
|
+
|
|
214
|
+
cmd_arg = cmd_arg.to_s
|
|
215
|
+
patterns.any? { |p| cmd_arg.include?(p.to_s) }
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def path(*globs)
|
|
220
|
+
->(ctx) do
|
|
221
|
+
args = ctx[:args]
|
|
222
|
+
path_arg = args[:path] || args['path'] ||
|
|
223
|
+
args[:file] || args['file'] ||
|
|
224
|
+
args[:target] || args['target'] ||
|
|
225
|
+
args[:file_path] || args['file_path'] ||
|
|
226
|
+
args[:target_file] || args['target_file']
|
|
227
|
+
return false unless path_arg
|
|
228
|
+
|
|
229
|
+
path_arg = path_arg.to_s
|
|
230
|
+
globs.any? { |g| File.fnmatch?(g.to_s, path_arg) }
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def args_match(**matchers)
|
|
235
|
+
->(ctx) do
|
|
236
|
+
args = ctx[:args]
|
|
237
|
+
matchers.any? do |k, v|
|
|
238
|
+
val = args[k.to_sym] || args[k.to_s]
|
|
239
|
+
val && v.match?(val.to_s)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# ------------------------------------------------------------------
|
|
245
|
+
# Evaluation engine
|
|
246
|
+
# ------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
def evaluate(tool_name, args = {})
|
|
249
|
+
matching_rules = @rules.select { |r| r.matches?(tool_name, args) }
|
|
250
|
+
best_rule = matching_rules.max_by(&:precedence)
|
|
251
|
+
|
|
252
|
+
if best_rule
|
|
253
|
+
if best_rule.action == :confirm
|
|
254
|
+
handler = best_rule.handler || @confirm_handler
|
|
255
|
+
if handler
|
|
256
|
+
ctx = { name: tool_name, args: args }
|
|
257
|
+
result = handler.call(ctx)
|
|
258
|
+
{ status: result ? :allow : :deny }
|
|
259
|
+
else
|
|
260
|
+
{ status: :deny }
|
|
261
|
+
end
|
|
262
|
+
elsif best_rule.action == :deny
|
|
263
|
+
{ status: :deny, reason: "Denied by policy" }
|
|
264
|
+
else
|
|
265
|
+
{ status: :allow }
|
|
266
|
+
end
|
|
267
|
+
else
|
|
268
|
+
{ status: :deny } # Default to deny if no rules match
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|
data/lib/antigravity.rb
CHANGED
|
@@ -14,6 +14,7 @@ require_relative "antigravity/config"
|
|
|
14
14
|
require_relative "antigravity/message"
|
|
15
15
|
require_relative "antigravity/protocol"
|
|
16
16
|
require_relative "antigravity/harness"
|
|
17
|
+
require_relative "antigravity/policy"
|
|
17
18
|
require_relative "antigravity/hooks"
|
|
18
19
|
require_relative "antigravity/guards"
|
|
19
20
|
require_relative "antigravity/sidecar"
|
|
@@ -26,6 +27,8 @@ require_relative "antigravity/connection/binary_fetcher"
|
|
|
26
27
|
require_relative "antigravity/connection/websocket_client"
|
|
27
28
|
require_relative "antigravity/connection/local_connection"
|
|
28
29
|
require_relative "antigravity/conversation"
|
|
30
|
+
require_relative "antigravity/colors"
|
|
31
|
+
require_relative "antigravity/lifecycle_logger"
|
|
29
32
|
require_relative "antigravity/agent"
|
|
30
33
|
|
|
31
34
|
module Antigravity
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: antigravity-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Riccardo Carlesso
|
|
@@ -63,6 +63,7 @@ files:
|
|
|
63
63
|
- lib/antigravity/agent.rb
|
|
64
64
|
- lib/antigravity/base.rb
|
|
65
65
|
- lib/antigravity/client.rb
|
|
66
|
+
- lib/antigravity/colors.rb
|
|
66
67
|
- lib/antigravity/config.rb
|
|
67
68
|
- lib/antigravity/connection/binary_fetcher.rb
|
|
68
69
|
- lib/antigravity/connection/local_connection.rb
|
|
@@ -73,7 +74,10 @@ files:
|
|
|
73
74
|
- lib/antigravity/guards.rb
|
|
74
75
|
- lib/antigravity/harness.rb
|
|
75
76
|
- lib/antigravity/hooks.rb
|
|
77
|
+
- lib/antigravity/lifecycle_logger.rb
|
|
76
78
|
- lib/antigravity/message.rb
|
|
79
|
+
- lib/antigravity/policy.rb
|
|
80
|
+
- lib/antigravity/policy/constants.rb
|
|
77
81
|
- lib/antigravity/protocol.rb
|
|
78
82
|
- lib/antigravity/sidecar.rb
|
|
79
83
|
- lib/antigravity/skill.rb
|