agent-harness 0.5.8 → 0.5.9
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/.release-please-manifest.json +1 -1
- data/AUDIT_DISPOSITION.md +111 -0
- data/CHANGELOG.md +11 -0
- data/lib/agent_harness/command_executor.rb +450 -13
- data/lib/agent_harness/docker_command_executor.rb +499 -8
- data/lib/agent_harness/error_taxonomy.rb +4 -4
- data/lib/agent_harness/execution_preparation.rb +64 -0
- data/lib/agent_harness/provider_runtime.rb +38 -11
- data/lib/agent_harness/providers/adapter.rb +8 -1
- data/lib/agent_harness/providers/aider.rb +17 -1
- data/lib/agent_harness/providers/anthropic.rb +21 -12
- data/lib/agent_harness/providers/base.rb +34 -5
- data/lib/agent_harness/providers/codex.rb +22 -6
- data/lib/agent_harness/providers/cursor.rb +3 -1
- data/lib/agent_harness/providers/gemini.rb +12 -6
- data/lib/agent_harness/providers/kilocode.rb +16 -1
- data/lib/agent_harness/providers/opencode.rb +55 -3
- data/lib/agent_harness/version.rb +1 -1
- data/lib/agent_harness.rb +1 -0
- metadata +3 -1
|
@@ -35,11 +35,15 @@ module AgentHarness
|
|
|
35
35
|
# @param unset_env [Array<String>] environment variable names to remove from inherited env
|
|
36
36
|
# @param metadata [Hash] arbitrary provider-specific data
|
|
37
37
|
def initialize(model: nil, base_url: nil, api_provider: nil, env: {}, flags: [], unset_env: [], metadata: {})
|
|
38
|
+
validate_optional_string!(:model, model)
|
|
39
|
+
validate_optional_string!(:base_url, base_url)
|
|
40
|
+
validate_optional_string!(:api_provider, api_provider)
|
|
41
|
+
|
|
38
42
|
@model = model
|
|
39
43
|
@base_url = base_url
|
|
40
44
|
@api_provider = api_provider
|
|
41
45
|
|
|
42
|
-
env_hash = env
|
|
46
|
+
env_hash = env.nil? ? {} : env
|
|
43
47
|
unless env_hash.is_a?(Hash)
|
|
44
48
|
raise ArgumentError, "env must be a Hash (got #{env_hash.class})"
|
|
45
49
|
end
|
|
@@ -52,7 +56,7 @@ module AgentHarness
|
|
|
52
56
|
end
|
|
53
57
|
@env = normalized_env.freeze
|
|
54
58
|
|
|
55
|
-
normalized_flags = flags
|
|
59
|
+
normalized_flags = flags.nil? ? [] : flags
|
|
56
60
|
unless normalized_flags.is_a?(Array)
|
|
57
61
|
raise ArgumentError, "flags must be an Array (got #{normalized_flags.class})"
|
|
58
62
|
end
|
|
@@ -65,7 +69,7 @@ module AgentHarness
|
|
|
65
69
|
end
|
|
66
70
|
@flags = normalized_flags.freeze
|
|
67
71
|
|
|
68
|
-
metadata_hash = metadata
|
|
72
|
+
metadata_hash = metadata.nil? ? {} : metadata
|
|
69
73
|
unless metadata_hash.is_a?(Hash)
|
|
70
74
|
raise ArgumentError, "metadata must be a Hash (got #{metadata_hash.class})"
|
|
71
75
|
end
|
|
@@ -74,7 +78,7 @@ module AgentHarness
|
|
|
74
78
|
# Unset environment variables for the request. These are variable names that
|
|
75
79
|
# should be removed from the inherited environment before the provider
|
|
76
80
|
# command runs.
|
|
77
|
-
unset_array = unset_env
|
|
81
|
+
unset_array = unset_env.nil? ? [] : unset_env
|
|
78
82
|
unless unset_array.is_a?(Array)
|
|
79
83
|
raise ArgumentError, "unset_env must be an Array (got #{unset_array.class})"
|
|
80
84
|
end
|
|
@@ -96,14 +100,19 @@ module AgentHarness
|
|
|
96
100
|
def self.from_hash(hash)
|
|
97
101
|
raise ArgumentError, "expected a Hash, got #{hash.class}" unless hash.is_a?(Hash)
|
|
98
102
|
|
|
103
|
+
env_val = hash_value(hash, :env)
|
|
104
|
+
flags_val = hash_value(hash, :flags)
|
|
105
|
+
unset_env_val = hash_value(hash, :unset_env)
|
|
106
|
+
metadata_val = hash_value(hash, :metadata)
|
|
107
|
+
|
|
99
108
|
new(
|
|
100
|
-
model: hash
|
|
101
|
-
base_url: hash
|
|
102
|
-
api_provider: hash
|
|
103
|
-
env:
|
|
104
|
-
flags:
|
|
105
|
-
unset_env:
|
|
106
|
-
metadata:
|
|
109
|
+
model: hash_value(hash, :model),
|
|
110
|
+
base_url: hash_value(hash, :base_url),
|
|
111
|
+
api_provider: hash_value(hash, :api_provider),
|
|
112
|
+
env: env_val.nil? ? {} : env_val,
|
|
113
|
+
flags: flags_val.nil? ? [] : flags_val,
|
|
114
|
+
unset_env: unset_env_val.nil? ? [] : unset_env_val,
|
|
115
|
+
metadata: metadata_val.nil? ? {} : metadata_val
|
|
107
116
|
)
|
|
108
117
|
end
|
|
109
118
|
|
|
@@ -128,5 +137,23 @@ module AgentHarness
|
|
|
128
137
|
model.nil? && base_url.nil? && api_provider.nil? &&
|
|
129
138
|
env.empty? && flags.empty? && metadata.empty? && unset_env.empty?
|
|
130
139
|
end
|
|
140
|
+
|
|
141
|
+
private_class_method def self.hash_value(hash, key)
|
|
142
|
+
sym_value = hash[key]
|
|
143
|
+
str_value = hash[key.to_s]
|
|
144
|
+
# Prefer the symbol key; fall back to the string key only when the
|
|
145
|
+
# symbol key is nil (not just falsy) so that an explicit `false` is
|
|
146
|
+
# not silently discarded.
|
|
147
|
+
sym_value.nil? ? str_value : sym_value
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
def validate_optional_string!(name, value)
|
|
153
|
+
return if value.nil?
|
|
154
|
+
return if value.is_a?(String)
|
|
155
|
+
|
|
156
|
+
raise ArgumentError, "#{name} must be a String or nil (got #{value.class})"
|
|
157
|
+
end
|
|
131
158
|
end
|
|
132
159
|
end
|
|
@@ -638,7 +638,14 @@ module AgentHarness
|
|
|
638
638
|
entry.is_a?(Array) ? "#{entry[0]} #{entry[1]}" : entry
|
|
639
639
|
end
|
|
640
640
|
parsed_requirement = Gem::Requirement.new(*requirement_args)
|
|
641
|
-
|
|
641
|
+
parsed_version = begin
|
|
642
|
+
Gem::Version.new(version)
|
|
643
|
+
rescue ArgumentError
|
|
644
|
+
raise ArgumentError,
|
|
645
|
+
"Unsupported #{provider_name} CLI version #{version.inspect}; " \
|
|
646
|
+
"supported versions must satisfy #{parsed_requirement}"
|
|
647
|
+
end
|
|
648
|
+
unless parsed_requirement.satisfied_by?(parsed_version)
|
|
642
649
|
raise ArgumentError,
|
|
643
650
|
"Unsupported #{provider_name} CLI version #{version.inspect}; " \
|
|
644
651
|
"supported versions must satisfy #{parsed_requirement}"
|
|
@@ -62,7 +62,23 @@ module AgentHarness
|
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
def installation_contract(version: SUPPORTED_CLI_VERSION)
|
|
65
|
-
|
|
65
|
+
version = version.strip if version.respond_to?(:strip)
|
|
66
|
+
|
|
67
|
+
unless version.is_a?(String) && !version.empty?
|
|
68
|
+
raise ArgumentError,
|
|
69
|
+
"Unsupported Aider CLI version #{version.inspect}; " \
|
|
70
|
+
"supported versions must satisfy #{SUPPORTED_CLI_REQUIREMENT}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
parsed_version = begin
|
|
74
|
+
Gem::Version.new(version)
|
|
75
|
+
rescue ArgumentError
|
|
76
|
+
raise ArgumentError,
|
|
77
|
+
"Unsupported Aider CLI version #{version.inspect}; " \
|
|
78
|
+
"supported versions must satisfy #{SUPPORTED_CLI_REQUIREMENT}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
unless SUPPORTED_CLI_REQUIREMENT.satisfied_by?(parsed_version)
|
|
66
82
|
raise ArgumentError,
|
|
67
83
|
"Unsupported Aider CLI version #{version.inspect}; " \
|
|
68
84
|
"supported versions must satisfy #{SUPPORTED_CLI_REQUIREMENT}"
|
|
@@ -33,7 +33,8 @@ module AgentHarness
|
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def install_contract(version: nil)
|
|
36
|
-
target_version = version
|
|
36
|
+
target_version = version.nil? ? SUPPORTED_CLI_VERSION : version
|
|
37
|
+
target_version = target_version.strip if target_version.respond_to?(:strip)
|
|
37
38
|
validate_version!(target_version)
|
|
38
39
|
version_requirement = SUPPORTED_CLI_REQUIREMENT.requirements
|
|
39
40
|
.map { |op, ver| "#{op} #{ver}" }
|
|
@@ -163,18 +164,26 @@ module AgentHarness
|
|
|
163
164
|
private
|
|
164
165
|
|
|
165
166
|
def validate_version!(version)
|
|
166
|
-
|
|
167
|
+
unless version.is_a?(String) && !version.strip.empty?
|
|
168
|
+
raise ArgumentError, "Invalid version: #{version.inspect}. " \
|
|
169
|
+
"Must be a semver string (e.g. '2.1.92'), optional pre-release suffix, or a channel token ('latest', 'stable')."
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
version_str = version.strip
|
|
167
173
|
|
|
168
174
|
unless VALID_VERSION_PATTERN.match?(version_str)
|
|
169
175
|
raise ArgumentError, "Invalid version: #{version.inspect}. " \
|
|
170
176
|
"Must be a semver string (e.g. '2.1.92'), optional pre-release suffix, or a channel token ('latest', 'stable')."
|
|
171
177
|
end
|
|
172
178
|
|
|
173
|
-
# Channel tokens are not concrete versions; skip requirement check.
|
|
174
179
|
return if %w[latest stable].include?(version_str)
|
|
175
180
|
|
|
176
|
-
|
|
177
|
-
|
|
181
|
+
gem_version = begin
|
|
182
|
+
Gem::Version.new(version_str)
|
|
183
|
+
rescue ArgumentError
|
|
184
|
+
raise ArgumentError, "Invalid version: #{version.inspect}. " \
|
|
185
|
+
"Must be a semver string (e.g. '2.1.92'), optional pre-release suffix, or a channel token ('latest', 'stable')."
|
|
186
|
+
end
|
|
178
187
|
return if SUPPORTED_CLI_REQUIREMENT.satisfied_by?(gem_version)
|
|
179
188
|
|
|
180
189
|
raise ArgumentError, "Version #{version.inspect} is outside the supported range " \
|
|
@@ -334,7 +343,7 @@ module AgentHarness
|
|
|
334
343
|
rate_limited: [
|
|
335
344
|
/rate.?limit/i,
|
|
336
345
|
/too.?many.?requests/i,
|
|
337
|
-
|
|
346
|
+
/\b429\b/,
|
|
338
347
|
/overloaded/i,
|
|
339
348
|
/session.?limit/i
|
|
340
349
|
],
|
|
@@ -343,7 +352,7 @@ module AgentHarness
|
|
|
343
352
|
/authentication.*error/i,
|
|
344
353
|
/invalid.*api.*key/i,
|
|
345
354
|
/unauthorized/i,
|
|
346
|
-
|
|
355
|
+
/\b401\b/,
|
|
347
356
|
/session.*expired/i,
|
|
348
357
|
/not.*logged.*in/i,
|
|
349
358
|
/login.*required/i,
|
|
@@ -359,17 +368,17 @@ module AgentHarness
|
|
|
359
368
|
/connection.*reset/i,
|
|
360
369
|
/temporary.*error/i,
|
|
361
370
|
/service.*unavailable/i,
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
/
|
|
371
|
+
/\b503\b/,
|
|
372
|
+
/\b502\b/,
|
|
373
|
+
/\b504\b/
|
|
365
374
|
],
|
|
366
375
|
permanent: [
|
|
367
376
|
/invalid.*model/i,
|
|
368
377
|
/unsupported.*operation/i,
|
|
369
378
|
/not.*found/i,
|
|
370
|
-
|
|
379
|
+
/\b404\b/,
|
|
371
380
|
/bad.*request/i,
|
|
372
|
-
|
|
381
|
+
/\b400\b/,
|
|
373
382
|
/model.*deprecated/i,
|
|
374
383
|
/end-of-life/i
|
|
375
384
|
]
|
|
@@ -41,7 +41,7 @@ module AgentHarness
|
|
|
41
41
|
rate_limited: [
|
|
42
42
|
/rate.?limit/i,
|
|
43
43
|
/too.?many.?requests/i,
|
|
44
|
-
/
|
|
44
|
+
/\b429\b/
|
|
45
45
|
],
|
|
46
46
|
auth_expired: [
|
|
47
47
|
/invalid.*api.*key/i,
|
|
@@ -57,8 +57,8 @@ module AgentHarness
|
|
|
57
57
|
/timeout/i,
|
|
58
58
|
/connection.*error/i,
|
|
59
59
|
/service.*unavailable/i,
|
|
60
|
-
|
|
61
|
-
/
|
|
60
|
+
/\b503\b/,
|
|
61
|
+
/\b502\b/
|
|
62
62
|
]
|
|
63
63
|
}.tap { |patterns| patterns.each_value(&:freeze) }.freeze
|
|
64
64
|
|
|
@@ -98,6 +98,8 @@ module AgentHarness
|
|
|
98
98
|
# @option options [ProviderRuntime, Hash, nil] :provider_runtime per-request
|
|
99
99
|
# runtime overrides (model, base_url, api_provider, env, flags, metadata).
|
|
100
100
|
# A plain Hash is automatically coerced into a ProviderRuntime.
|
|
101
|
+
# Providers can derive request-scoped execution preparation from this
|
|
102
|
+
# runtime to materialize config files or other bootstrap state.
|
|
101
103
|
# @return [Response] the response
|
|
102
104
|
def send_message(prompt:, **options)
|
|
103
105
|
log_debug("send_message_start", prompt_length: prompt.length, options: options.keys)
|
|
@@ -111,6 +113,7 @@ module AgentHarness
|
|
|
111
113
|
|
|
112
114
|
# Build command
|
|
113
115
|
command = build_command(prompt, options)
|
|
116
|
+
preparation = build_execution_preparation(options)
|
|
114
117
|
|
|
115
118
|
# Calculate timeout
|
|
116
119
|
timeout = options[:timeout] || @config.timeout || default_timeout
|
|
@@ -121,6 +124,7 @@ module AgentHarness
|
|
|
121
124
|
command,
|
|
122
125
|
timeout: timeout,
|
|
123
126
|
env: build_env(options),
|
|
127
|
+
preparation: preparation,
|
|
124
128
|
**command_execution_options(options)
|
|
125
129
|
)
|
|
126
130
|
duration = Time.now - start_time
|
|
@@ -210,6 +214,17 @@ module AgentHarness
|
|
|
210
214
|
env
|
|
211
215
|
end
|
|
212
216
|
|
|
217
|
+
# Build structured runtime preparation for the executor.
|
|
218
|
+
#
|
|
219
|
+
# Provider subclasses can override to request request-scoped file writes
|
|
220
|
+
# or other bootstrap work without shell-wrapping the main command.
|
|
221
|
+
#
|
|
222
|
+
# @param options [Hash] options
|
|
223
|
+
# @return [ExecutionPreparation, nil] preparation contract or nil
|
|
224
|
+
def build_execution_preparation(options)
|
|
225
|
+
nil
|
|
226
|
+
end
|
|
227
|
+
|
|
213
228
|
# Parse CLI output into Response - override in subclasses
|
|
214
229
|
#
|
|
215
230
|
# Combines stdout and stderr for error classification so that
|
|
@@ -309,8 +324,22 @@ module AgentHarness
|
|
|
309
324
|
execution_options
|
|
310
325
|
end
|
|
311
326
|
|
|
312
|
-
def execute_with_timeout(command, timeout:, env:, stdin_data: nil, **execution_options)
|
|
313
|
-
|
|
327
|
+
def execute_with_timeout(command, timeout:, env:, preparation: nil, stdin_data: nil, **execution_options)
|
|
328
|
+
kwargs = {timeout: timeout, env: env}
|
|
329
|
+
kwargs[:stdin_data] = stdin_data unless stdin_data.nil?
|
|
330
|
+
kwargs[:preparation] = preparation unless preparation.nil?
|
|
331
|
+
kwargs.merge!(execution_options)
|
|
332
|
+
|
|
333
|
+
@executor.execute(command, **kwargs)
|
|
334
|
+
rescue ArgumentError => e
|
|
335
|
+
unknown_keyword_message = e.message
|
|
336
|
+
raise unless unknown_keyword_message.start_with?("unknown keyword", "unknown keywords")
|
|
337
|
+
raise unless unknown_keyword_message.include?(":preparation")
|
|
338
|
+
|
|
339
|
+
raise ProviderError.new(
|
|
340
|
+
"Injected executor #{@executor.class}#execute must accept the preparation: keyword argument",
|
|
341
|
+
original_error: e
|
|
342
|
+
)
|
|
314
343
|
end
|
|
315
344
|
|
|
316
345
|
def track_tokens(response)
|
|
@@ -63,7 +63,23 @@ module AgentHarness
|
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
def installation_contract(version: SUPPORTED_CLI_VERSION)
|
|
66
|
-
|
|
66
|
+
version = version.strip if version.respond_to?(:strip)
|
|
67
|
+
|
|
68
|
+
unless version.is_a?(String) && !version.empty?
|
|
69
|
+
raise ArgumentError,
|
|
70
|
+
"Unsupported Codex CLI version #{version.inspect}; " \
|
|
71
|
+
"supported versions must satisfy #{SUPPORTED_CLI_REQUIREMENT}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
parsed_version = begin
|
|
75
|
+
Gem::Version.new(version)
|
|
76
|
+
rescue ArgumentError
|
|
77
|
+
raise ArgumentError,
|
|
78
|
+
"Unsupported Codex CLI version #{version.inspect}; " \
|
|
79
|
+
"supported versions must satisfy #{SUPPORTED_CLI_REQUIREMENT}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
unless SUPPORTED_CLI_REQUIREMENT.satisfied_by?(parsed_version)
|
|
67
83
|
raise ArgumentError,
|
|
68
84
|
"Unsupported Codex CLI version #{version.inspect}; " \
|
|
69
85
|
"supported versions must satisfy #{SUPPORTED_CLI_REQUIREMENT}"
|
|
@@ -156,7 +172,7 @@ module AgentHarness
|
|
|
156
172
|
|
|
157
173
|
def error_patterns
|
|
158
174
|
COMMON_ERROR_PATTERNS.merge(
|
|
159
|
-
auth_expired: COMMON_ERROR_PATTERNS[:auth_expired] + [
|
|
175
|
+
auth_expired: COMMON_ERROR_PATTERNS[:auth_expired] + [/\b401\b/, /incorrect.*api.*key/i],
|
|
160
176
|
transient: COMMON_ERROR_PATTERNS[:transient] + [/connection.*reset/i],
|
|
161
177
|
sandbox_failure: [
|
|
162
178
|
/bwrap.*no permissions/i,
|
|
@@ -172,7 +188,7 @@ module AgentHarness
|
|
|
172
188
|
if api_key.strip.start_with?("sk-")
|
|
173
189
|
return {valid: true, expires_at: nil, error: nil, auth_method: :api_key}
|
|
174
190
|
else
|
|
175
|
-
return {valid: false, expires_at: nil, error: "OPENAI_API_KEY is set but does not appear to be a valid OpenAI API key"}
|
|
191
|
+
return {valid: false, expires_at: nil, error: "OPENAI_API_KEY is set but does not appear to be a valid OpenAI API key", auth_method: nil}
|
|
176
192
|
end
|
|
177
193
|
end
|
|
178
194
|
|
|
@@ -183,14 +199,14 @@ module AgentHarness
|
|
|
183
199
|
if key.strip.start_with?("sk-")
|
|
184
200
|
return {valid: true, expires_at: nil, error: nil, auth_method: :config_file}
|
|
185
201
|
else
|
|
186
|
-
return {valid: false, expires_at: nil, error: "Config file API key is set but does not appear to be a valid OpenAI API key"}
|
|
202
|
+
return {valid: false, expires_at: nil, error: "Config file API key is set but does not appear to be a valid OpenAI API key", auth_method: nil}
|
|
187
203
|
end
|
|
188
204
|
end
|
|
189
205
|
end
|
|
190
206
|
|
|
191
|
-
{valid: false, expires_at: nil, error: "No OpenAI API key found. Set OPENAI_API_KEY or configure in #{codex_config_path}"}
|
|
207
|
+
{valid: false, expires_at: nil, error: "No OpenAI API key found. Set OPENAI_API_KEY or configure in #{codex_config_path}", auth_method: nil}
|
|
192
208
|
rescue IOError, JSON::ParserError => e
|
|
193
|
-
{valid: false, expires_at: nil, error: e.message}
|
|
209
|
+
{valid: false, expires_at: nil, error: e.message, auth_method: nil}
|
|
194
210
|
end
|
|
195
211
|
|
|
196
212
|
def health_status
|
|
@@ -228,7 +228,7 @@ module AgentHarness
|
|
|
228
228
|
rate_limited: [
|
|
229
229
|
/rate.?limit/i,
|
|
230
230
|
/too.?many.?requests/i,
|
|
231
|
-
/
|
|
231
|
+
/\b429\b/
|
|
232
232
|
],
|
|
233
233
|
auth_expired: [
|
|
234
234
|
/authentication.*error/i,
|
|
@@ -264,12 +264,14 @@ module AgentHarness
|
|
|
264
264
|
|
|
265
265
|
# Execute command with prompt on stdin
|
|
266
266
|
env = build_env(options)
|
|
267
|
+
preparation = build_execution_preparation(options)
|
|
267
268
|
start_time = Time.now
|
|
268
269
|
result = execute_with_timeout(
|
|
269
270
|
command,
|
|
270
271
|
timeout: timeout,
|
|
271
272
|
env: env,
|
|
272
273
|
stdin_data: prompt,
|
|
274
|
+
preparation: preparation,
|
|
273
275
|
**command_execution_options(options)
|
|
274
276
|
)
|
|
275
277
|
duration = Time.now - start_time
|
|
@@ -40,6 +40,12 @@ module AgentHarness
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
def install_contract(version: SUPPORTED_CLI_VERSION)
|
|
43
|
+
version = version.strip if version.respond_to?(:strip)
|
|
44
|
+
|
|
45
|
+
unless version.is_a?(String) && !version.empty?
|
|
46
|
+
raise ArgumentError, "Unsupported Gemini CLI version #{version.inspect}. Supported requirement: #{SUPPORTED_CLI_REQUIREMENT}"
|
|
47
|
+
end
|
|
48
|
+
|
|
43
49
|
parsed_version = begin
|
|
44
50
|
Gem::Version.new(version)
|
|
45
51
|
rescue ArgumentError
|
|
@@ -179,7 +185,7 @@ module AgentHarness
|
|
|
179
185
|
rate_limited: [
|
|
180
186
|
/rate.?limit/i,
|
|
181
187
|
/quota.?exceeded/i,
|
|
182
|
-
/
|
|
188
|
+
/\b429\b/
|
|
183
189
|
],
|
|
184
190
|
auth_expired: [
|
|
185
191
|
/authentication/i,
|
|
@@ -193,7 +199,7 @@ module AgentHarness
|
|
|
193
199
|
transient: [
|
|
194
200
|
/timeout/i,
|
|
195
201
|
/temporary/i,
|
|
196
|
-
/
|
|
202
|
+
/\b503\b/
|
|
197
203
|
]
|
|
198
204
|
}
|
|
199
205
|
end
|
|
@@ -205,21 +211,21 @@ module AgentHarness
|
|
|
205
211
|
end
|
|
206
212
|
|
|
207
213
|
credentials = read_gemini_credentials
|
|
208
|
-
return {valid: false, expires_at: nil, error: "No Gemini credentials found. Run 'gemini auth login' or set GEMINI_API_KEY or GOOGLE_API_KEY"} unless credentials
|
|
214
|
+
return {valid: false, expires_at: nil, error: "No Gemini credentials found. Run 'gemini auth login' or set GEMINI_API_KEY or GOOGLE_API_KEY", auth_method: nil} unless credentials
|
|
209
215
|
|
|
210
216
|
token = credentials["access_token"] || credentials["oauth_token"]
|
|
211
217
|
unless token.is_a?(String) && !token.strip.empty?
|
|
212
|
-
return {valid: false, expires_at: nil, error: "No authentication token in Gemini credentials"}
|
|
218
|
+
return {valid: false, expires_at: nil, error: "No authentication token in Gemini credentials", auth_method: nil}
|
|
213
219
|
end
|
|
214
220
|
|
|
215
221
|
expires_at = parse_gemini_expiry(credentials)
|
|
216
222
|
if expires_at && expires_at < Time.now
|
|
217
|
-
{valid: false, expires_at: expires_at, error: "Gemini session expired. Run 'gemini auth login' to re-authenticate"}
|
|
223
|
+
{valid: false, expires_at: expires_at, error: "Gemini session expired. Run 'gemini auth login' to re-authenticate", auth_method: :oauth}
|
|
218
224
|
else
|
|
219
225
|
{valid: true, expires_at: expires_at, error: nil, auth_method: :oauth}
|
|
220
226
|
end
|
|
221
227
|
rescue IOError, JSON::ParserError => e
|
|
222
|
-
{valid: false, expires_at: nil, error: e.message}
|
|
228
|
+
{valid: false, expires_at: nil, error: e.message, auth_method: nil}
|
|
223
229
|
end
|
|
224
230
|
|
|
225
231
|
def health_status
|
|
@@ -41,6 +41,7 @@ module AgentHarness
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def installation_contract(version: DEFAULT_VERSION)
|
|
44
|
+
version = version.strip if version.respond_to?(:strip)
|
|
44
45
|
validate_install_version!(version)
|
|
45
46
|
package_spec = "#{PACKAGE_NAME}@#{version}"
|
|
46
47
|
|
|
@@ -67,8 +68,22 @@ module AgentHarness
|
|
|
67
68
|
private
|
|
68
69
|
|
|
69
70
|
def validate_install_version!(version)
|
|
71
|
+
unless version.is_a?(String) && !version.strip.empty?
|
|
72
|
+
raise ArgumentError,
|
|
73
|
+
"Unsupported Kilocode CLI version #{version.inspect}; " \
|
|
74
|
+
"supported versions must satisfy #{SUPPORTED_VERSION_REQUIREMENT}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
parsed_version = begin
|
|
78
|
+
Gem::Version.new(version)
|
|
79
|
+
rescue ArgumentError
|
|
80
|
+
raise ArgumentError,
|
|
81
|
+
"Unsupported Kilocode CLI version #{version.inspect}; " \
|
|
82
|
+
"supported versions must satisfy #{SUPPORTED_VERSION_REQUIREMENT}"
|
|
83
|
+
end
|
|
84
|
+
|
|
70
85
|
requirement = Gem::Requirement.new(SUPPORTED_VERSION_REQUIREMENT)
|
|
71
|
-
return if requirement.satisfied_by?(
|
|
86
|
+
return if requirement.satisfied_by?(parsed_version)
|
|
72
87
|
|
|
73
88
|
raise ArgumentError,
|
|
74
89
|
"Unsupported Kilocode CLI version #{version.inspect}; " \
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
3
5
|
module AgentHarness
|
|
4
6
|
module Providers
|
|
5
7
|
# OpenCode CLI provider
|
|
@@ -99,11 +101,13 @@ module AgentHarness
|
|
|
99
101
|
raise ArgumentError, unsupported_version_message(version) unless version.is_a?(String) && !version.strip.empty?
|
|
100
102
|
|
|
101
103
|
normalized_version = version.strip
|
|
102
|
-
parsed_version =
|
|
104
|
+
parsed_version = begin
|
|
105
|
+
Gem::Version.new(normalized_version)
|
|
106
|
+
rescue ArgumentError
|
|
107
|
+
raise ArgumentError, unsupported_version_message(version)
|
|
108
|
+
end
|
|
103
109
|
return normalized_version if SUPPORTED_CLI_REQUIREMENT.satisfied_by?(parsed_version)
|
|
104
110
|
|
|
105
|
-
raise ArgumentError, unsupported_version_message(version)
|
|
106
|
-
rescue ArgumentError
|
|
107
111
|
raise ArgumentError, unsupported_version_message(version)
|
|
108
112
|
end
|
|
109
113
|
|
|
@@ -183,9 +187,57 @@ module AgentHarness
|
|
|
183
187
|
env
|
|
184
188
|
end
|
|
185
189
|
|
|
190
|
+
def build_execution_preparation(options)
|
|
191
|
+
runtime = options[:provider_runtime]
|
|
192
|
+
return nil unless runtime
|
|
193
|
+
|
|
194
|
+
config_payload = opencode_config_payload(runtime)
|
|
195
|
+
return nil unless config_payload
|
|
196
|
+
|
|
197
|
+
ExecutionPreparation.new(
|
|
198
|
+
file_writes: [
|
|
199
|
+
{
|
|
200
|
+
path: opencode_config_path(runtime),
|
|
201
|
+
content: serialize_opencode_config(config_payload),
|
|
202
|
+
mode: 0o600
|
|
203
|
+
}
|
|
204
|
+
]
|
|
205
|
+
)
|
|
206
|
+
end
|
|
207
|
+
|
|
186
208
|
def default_timeout
|
|
187
209
|
300
|
|
188
210
|
end
|
|
211
|
+
|
|
212
|
+
private
|
|
213
|
+
|
|
214
|
+
def opencode_config_payload(runtime)
|
|
215
|
+
metadata = runtime.metadata
|
|
216
|
+
config_extras = metadata[:config] || metadata["config"] || {}
|
|
217
|
+
unless config_extras.is_a?(Hash)
|
|
218
|
+
raise ArgumentError, "OpenCode runtime metadata config must be a Hash of provider-specific extras (got #{config_extras.class})"
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
payload = stringify_keys(config_extras)
|
|
222
|
+
payload["model"] = runtime.model if runtime.model
|
|
223
|
+
payload["provider"] = runtime.api_provider if runtime.api_provider
|
|
224
|
+
payload["baseURL"] = runtime.base_url if runtime.base_url
|
|
225
|
+
payload.empty? ? nil : payload
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def opencode_config_path(_runtime)
|
|
229
|
+
"~/.config/opencode/opencode.json"
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def serialize_opencode_config(payload)
|
|
233
|
+
JSON.pretty_generate(payload)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def stringify_keys(hash)
|
|
237
|
+
hash.each_with_object({}) do |(key, value), result|
|
|
238
|
+
result[key.to_s] = value
|
|
239
|
+
end
|
|
240
|
+
end
|
|
189
241
|
end
|
|
190
242
|
end
|
|
191
243
|
end
|
data/lib/agent_harness.rb
CHANGED
|
@@ -238,6 +238,7 @@ end
|
|
|
238
238
|
require_relative "agent_harness/errors"
|
|
239
239
|
require_relative "agent_harness/mcp_server"
|
|
240
240
|
require_relative "agent_harness/provider_runtime"
|
|
241
|
+
require_relative "agent_harness/execution_preparation"
|
|
241
242
|
require_relative "agent_harness/configuration"
|
|
242
243
|
require_relative "agent_harness/command_executor"
|
|
243
244
|
require_relative "agent_harness/docker_command_executor"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: agent-harness
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bart Agapinan
|
|
@@ -89,6 +89,7 @@ files:
|
|
|
89
89
|
- ".rubocop.yml"
|
|
90
90
|
- ".simplecov"
|
|
91
91
|
- ".tool-versions"
|
|
92
|
+
- AUDIT_DISPOSITION.md
|
|
92
93
|
- CHANGELOG.md
|
|
93
94
|
- CODE_OF_CONDUCT.md
|
|
94
95
|
- LICENSE.txt
|
|
@@ -104,6 +105,7 @@ files:
|
|
|
104
105
|
- lib/agent_harness/docker_command_executor.rb
|
|
105
106
|
- lib/agent_harness/error_taxonomy.rb
|
|
106
107
|
- lib/agent_harness/errors.rb
|
|
108
|
+
- lib/agent_harness/execution_preparation.rb
|
|
107
109
|
- lib/agent_harness/mcp_server.rb
|
|
108
110
|
- lib/agent_harness/orchestration/circuit_breaker.rb
|
|
109
111
|
- lib/agent_harness/orchestration/conductor.rb
|