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.
@@ -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[:model] || hash["model"],
101
- base_url: hash[:base_url] || hash["base_url"],
102
- api_provider: hash[:api_provider] || hash["api_provider"],
103
- env: hash[:env] || hash["env"] || {},
104
- flags: hash[:flags] || hash["flags"] || [],
105
- unset_env: hash[:unset_env] || hash["unset_env"] || [],
106
- metadata: hash[:metadata] || hash["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
- unless parsed_requirement.satisfied_by?(Gem::Version.new(version))
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
- unless SUPPORTED_CLI_REQUIREMENT.satisfied_by?(Gem::Version.new(version))
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 || SUPPORTED_CLI_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
- version_str = version.to_s
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
- # Validate concrete versions against the supported range.
177
- gem_version = Gem::Version.new(version_str)
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
- /429/,
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
- /401/,
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
- /503/,
363
- /502/,
364
- /504/
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
- /404/,
379
+ /\b404\b/,
371
380
  /bad.*request/i,
372
- /400/,
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
- /429/
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
- /503/,
61
- /502/
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
- @executor.execute(command, timeout: timeout, env: env, stdin_data: stdin_data, **execution_options)
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
- unless SUPPORTED_CLI_REQUIREMENT.satisfied_by?(Gem::Version.new(version))
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] + [/401/, /incorrect.*api.*key/i],
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
- /429/
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
- /429/
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
- /503/
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?(Gem::Version.new(version))
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 = Gem::Version.new(normalized_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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AgentHarness
4
- VERSION = "0.5.8"
4
+ VERSION = "0.5.9"
5
5
  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.8
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