agent-harness 0.5.6 → 0.5.7

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.
@@ -25,14 +25,18 @@ module AgentHarness
25
25
  #
26
26
  # @param timeout [Integer] timeout in seconds for each check
27
27
  # @return [Array<Hash>] health status for each provider
28
- def check_all(timeout: configured_timeout)
28
+ def check_all(timeout: configured_timeout, executor: nil, provider_runtime: nil)
29
+ raise ArgumentError, "provider_runtime is only supported for single-provider health checks" unless provider_runtime.nil?
30
+
29
31
  provider_names = if AgentHarness.configuration.providers.empty?
30
32
  Providers::Registry.instance.all
31
33
  else
32
34
  enabled_provider_names
33
35
  end
34
36
 
35
- provider_names.map { |name| check(name, timeout: timeout) }
37
+ provider_names.map do |name|
38
+ check(name, timeout: timeout, executor: executor, provider_runtime: provider_runtime)
39
+ end
36
40
  end
37
41
 
38
42
  # Check health of a single provider
@@ -40,32 +44,47 @@ module AgentHarness
40
44
  # @param provider_name [Symbol, String] the provider name
41
45
  # @param timeout [Integer] timeout in seconds
42
46
  # @return [Hash] health status with :name, :status, :message, :latency_ms keys
43
- def check(provider_name, timeout: configured_timeout)
47
+ def check(provider_name, timeout: configured_timeout, executor: nil, provider_runtime: nil)
44
48
  name = normalize_name(provider_name)
45
49
  start_time = monotonic_now
46
50
  timeout = validate_timeout(timeout)
47
51
 
48
- Timeout.timeout(timeout) do
49
- perform_check(name, start_time)
52
+ # Honor the provider smoke-test contract timeout when it exceeds
53
+ # the health-check timeout, so real CLI round trips are not
54
+ # falsely reported as timeouts.
55
+ outer_timeout = effective_check_timeout(name, timeout)
56
+
57
+ Timeout.timeout(outer_timeout) do
58
+ perform_check(
59
+ name,
60
+ start_time,
61
+ timeout: timeout,
62
+ executor: executor,
63
+ provider_runtime: provider_runtime
64
+ )
50
65
  end
51
66
  rescue Timeout::Error
52
67
  build_result(
53
68
  name: name,
54
69
  status: "error",
55
- message: "Health check timed out after #{timeout}s",
56
- start_time: start_time || monotonic_now
70
+ message: "Health check timed out after #{outer_timeout || timeout}s",
71
+ start_time: start_time || monotonic_now,
72
+ error_category: :timeout,
73
+ check: :timeout
57
74
  )
58
- rescue NotImplementedError => e
75
+ rescue NotImplementedError, ConfigurationError => e
59
76
  # NotImplementedError inherits from ScriptError, not StandardError,
60
77
  # so it must be rescued explicitly. Its messages are safe internal
61
- # setup errors (e.g., missing provider methods) that help users
62
- # diagnose configuration problems.
78
+ # setup errors (e.g., missing provider methods or malformed provider
79
+ # contracts) that help users diagnose configuration problems.
63
80
  AgentHarness.logger&.error("ProviderHealthCheck error for #{name}: #{e.class}")
64
81
  build_result(
65
82
  name: name,
66
83
  status: "error",
67
84
  message: "Health check failed: #{e.class}: #{e.message}",
68
- start_time: start_time || monotonic_now
85
+ start_time: start_time || monotonic_now,
86
+ error_category: :configuration,
87
+ check: :provider_health
69
88
  )
70
89
  rescue => e
71
90
  # Return a generic message to avoid leaking sensitive details
@@ -76,7 +95,9 @@ module AgentHarness
76
95
  name: name,
77
96
  status: "error",
78
97
  message: "Health check failed: #{e.class}",
79
- start_time: start_time || monotonic_now
98
+ start_time: start_time || monotonic_now,
99
+ error_category: :unknown,
100
+ check: :provider_health
80
101
  )
81
102
  end
82
103
 
@@ -148,7 +169,7 @@ module AgentHarness
148
169
  :unknown
149
170
  end
150
171
 
151
- def perform_check(provider_name, start_time)
172
+ def perform_check(provider_name, start_time, timeout:, executor:, provider_runtime:)
152
173
  # Step 1: Check provider is registered
153
174
  registry = Providers::Registry.instance
154
175
  unless registry.registered?(provider_name)
@@ -156,53 +177,83 @@ module AgentHarness
156
177
  name: provider_name,
157
178
  status: "error",
158
179
  message: "Provider not registered",
159
- start_time: start_time
180
+ start_time: start_time,
181
+ error_category: :installation,
182
+ check: :registration
160
183
  )
161
184
  end
162
185
 
163
- # Step 2: Check CLI availability
164
186
  klass = registry.get(provider_name)
165
- unless klass.available?
166
- return build_result(
167
- name: provider_name,
168
- status: "error",
169
- message: "CLI '#{klass.binary_name}' not found in PATH",
170
- start_time: start_time
171
- )
172
- end
187
+ provider_instance = build_provider(provider_name, klass, executor: executor)
188
+ host_preflight_allowed = host_preflight_allowed?(executor: executor, provider_runtime: provider_runtime)
173
189
 
174
- # Step 3: Check authentication
175
- # Treat "not implemented" auth status as degraded rather than error,
176
- # since most built-in providers don't implement auth_status hooks.
177
- # In either case, continue to steps 4/5 so health and config issues
178
- # are still surfaced for providers that lack an auth_status hook.
179
- auth = Authentication.auth_status(provider_name)
180
190
  auth_degraded = false
181
- unless auth[:valid]
182
- unless auth_not_implemented?(auth)
191
+ if host_preflight_allowed
192
+ # Step 2a: Honor the provider's `.available?` contract when running
193
+ # against the default host executor. Custom providers may enforce
194
+ # version or feature checks beyond simple PATH presence, so this
195
+ # catches cases where the binary exists but the provider considers
196
+ # itself unavailable. We skip this when a custom executor is
197
+ # supplied because `.available?` always queries the global
198
+ # executor, which may not reflect the caller's execution context.
199
+ if executor.nil? && !klass.available?
183
200
  return build_result(
184
201
  name: provider_name,
185
202
  status: "error",
186
- message: auth[:error] || "Authentication failed",
187
- start_time: start_time
203
+ message: "Provider '#{klass.binary_name}' is not available (#{klass}.available? returned false)",
204
+ start_time: start_time,
205
+ error_category: :installation,
206
+ check: :availability
188
207
  )
189
208
  end
190
- auth_degraded = true
191
- end
192
209
 
193
- # Step 4: Check provider-level health (e.g., endpoint reachability)
194
- # The Adapter default always returns {healthy: true}, so providers
195
- # that haven't implemented a real health check are reported as ok
196
- # with a note that the check is not implemented.
197
- provider_instance = build_provider(provider_name, klass)
198
- health = provider_instance.health_status
199
- unless health[:healthy]
200
- return build_result(
201
- name: provider_name,
202
- status: "degraded",
203
- message: health[:message] || "Provider health check failed",
204
- start_time: start_time
205
- )
210
+ # Step 2b: Verify the binary is findable by the effective executor.
211
+ unless provider_instance.executor.which(klass.binary_name)
212
+ return build_result(
213
+ name: provider_name,
214
+ status: "error",
215
+ message: "CLI '#{klass.binary_name}' not found in PATH",
216
+ start_time: start_time,
217
+ error_category: :installation,
218
+ check: :availability
219
+ )
220
+ end
221
+
222
+ # Step 3: Check authentication
223
+ # Treat "not implemented" auth status as degraded rather than error,
224
+ # since most built-in providers don't implement auth_status hooks.
225
+ # In either case, continue to steps 4/5 so health and config issues
226
+ # are still surfaced for providers that lack an auth_status hook.
227
+ auth = Authentication.auth_status(provider_name)
228
+ unless auth[:valid]
229
+ unless auth_not_implemented?(auth)
230
+ return build_result(
231
+ name: provider_name,
232
+ status: "error",
233
+ message: auth[:error] || "Authentication failed",
234
+ start_time: start_time,
235
+ error_category: :authentication,
236
+ check: :authentication
237
+ )
238
+ end
239
+ auth_degraded = true
240
+ end
241
+
242
+ # Step 4: Check provider-level health (e.g., endpoint reachability)
243
+ # The Adapter default always returns {healthy: true}, so providers
244
+ # that haven't implemented a real health check are reported as ok
245
+ # with a note that the check is not implemented.
246
+ health = provider_instance.health_status
247
+ unless health[:healthy]
248
+ return build_result(
249
+ name: provider_name,
250
+ status: "degraded",
251
+ message: health[:message] || "Provider health check failed",
252
+ start_time: start_time,
253
+ error_category: :transient,
254
+ check: :provider_health
255
+ )
256
+ end
206
257
  end
207
258
 
208
259
  # Step 5: Validate provider config
@@ -216,7 +267,52 @@ module AgentHarness
216
267
  name: provider_name,
217
268
  status: "degraded",
218
269
  message: "Configuration issues: #{errors_msg}",
219
- start_time: start_time
270
+ start_time: start_time,
271
+ error_category: :configuration,
272
+ check: :configuration
273
+ )
274
+ end
275
+
276
+ smoke_contract = provider_instance.smoke_test_contract
277
+ # Explicitly handle missing smoke-test contract when no custom smoke_test implementation
278
+ if smoke_contract.nil? && !provider_overrides_method?(provider_instance, :smoke_test)
279
+ message = if host_preflight_allowed && auth_degraded
280
+ "Auth status check not implemented; health and config checks passed (smoke test unavailable)"
281
+ elsif host_preflight_allowed && (provider_overrides_method?(provider_instance, :health_status) ||
282
+ provider_overrides_method?(provider_instance, :validate_config))
283
+ "Health and config checks passed (smoke test unavailable)"
284
+ elsif host_preflight_allowed
285
+ "Registered and authenticated; health/config checks use defaults and smoke test is unavailable"
286
+ elsif provider_overrides_method?(provider_instance, :validate_config)
287
+ "Configuration checks passed, but smoke test is unavailable for the supplied execution context"
288
+ else
289
+ "Smoke test is unavailable for the supplied execution context"
290
+ end
291
+
292
+ return build_result(
293
+ name: provider_name,
294
+ status: "degraded",
295
+ message: message,
296
+ start_time: start_time,
297
+ error_category: :configuration,
298
+ check: :smoke_test
299
+ )
300
+ end
301
+
302
+ # When a contract exists, pass nil so the adapter falls through to
303
+ # contract[:timeout]. When the provider overrides #smoke_test without
304
+ # publishing a contract, forward the validated health-check timeout so
305
+ # the override can honour it instead of running without any limit.
306
+ smoke_timeout = smoke_contract ? nil : timeout
307
+ smoke = provider_instance.smoke_test(timeout: smoke_timeout, provider_runtime: provider_runtime)
308
+ unless smoke[:ok]
309
+ return build_result(
310
+ name: provider_name,
311
+ status: smoke[:status] || "error",
312
+ message: smoke[:message] || "Smoke test failed",
313
+ start_time: start_time,
314
+ error_category: normalize_smoke_error_category(smoke[:error_category], smoke[:message]),
315
+ check: :smoke_test
220
316
  )
221
317
  end
222
318
 
@@ -225,23 +321,30 @@ module AgentHarness
225
321
  return build_result(
226
322
  name: provider_name,
227
323
  status: "degraded",
228
- message: "Auth status check not implemented; health and config checks passed",
229
- start_time: start_time
324
+ message: "Auth status check not implemented; health, config, and smoke tests passed",
325
+ start_time: start_time,
326
+ error_category: :authentication,
327
+ check: :authentication
230
328
  )
231
329
  end
232
330
 
233
- message = if provider_overrides_method?(provider_instance, :health_status) ||
331
+ message = if !host_preflight_allowed && provider_overrides_method?(provider_instance, :validate_config)
332
+ "Configuration and smoke test passed using the supplied execution context"
333
+ elsif !host_preflight_allowed
334
+ "Smoke test passed using the supplied execution context"
335
+ elsif provider_overrides_method?(provider_instance, :health_status) ||
234
336
  provider_overrides_method?(provider_instance, :validate_config)
235
337
  "All checks passed"
236
338
  else
237
- "Registered and authenticated (health/config checks use defaults)"
339
+ "Registered, authenticated, and smoke test passed (health/config checks use defaults)"
238
340
  end
239
341
 
240
342
  build_result(
241
343
  name: provider_name,
242
344
  status: "ok",
243
345
  message: message,
244
- start_time: start_time
346
+ start_time: start_time,
347
+ check: :smoke_test
245
348
  )
246
349
  end
247
350
 
@@ -258,25 +361,80 @@ module AgentHarness
258
361
  error.include?("not implemented")
259
362
  end
260
363
 
364
+ def host_preflight_allowed?(executor:, provider_runtime: nil)
365
+ effective_executor = executor || AgentHarness.configuration.command_executor
366
+ # Skip host preflight only when provider runtime has environment/config overrides
367
+ # that could conflict with host-level checks (env, base_url, api_provider, unset_env)
368
+ if provider_runtime
369
+ runtime = ProviderRuntime.wrap(provider_runtime)
370
+ return false if runtime && (!runtime.env.empty? || !runtime.unset_env.empty? || runtime.base_url || runtime.api_provider)
371
+ end
372
+ effective_executor.is_a?(CommandExecutor) && !effective_executor.is_a?(DockerCommandExecutor)
373
+ end
374
+
375
+ def effective_check_timeout(provider_name, base_timeout)
376
+ registry = Providers::Registry.instance
377
+ return base_timeout unless registry.registered?(provider_name)
378
+
379
+ contract = registry.smoke_test_contract(provider_name)
380
+ contract_timeout = contract&.dig(:timeout)
381
+ return base_timeout unless contract_timeout.is_a?(Numeric) && contract_timeout.positive?
382
+
383
+ [base_timeout, contract_timeout].max
384
+ end
385
+
386
+ def normalize_smoke_error_category(category, message)
387
+ normalized = if installation_failure_message?(message)
388
+ :installation
389
+ else
390
+ category || ErrorTaxonomy.classify_message(message)
391
+ end
392
+
393
+ case normalized&.to_sym
394
+ when :installation
395
+ :installation
396
+ when :auth_expired, :authentication
397
+ :authentication
398
+ when :rate_limited, :rate_limit
399
+ :rate_limit
400
+ when :quota_exceeded, :quota
401
+ :quota
402
+ when :timeout
403
+ :timeout
404
+ when :transient
405
+ :transient
406
+ when :sandbox_failure, :configuration, :permanent
407
+ :configuration
408
+ else
409
+ :unknown
410
+ end
411
+ end
412
+
413
+ def installation_failure_message?(message)
414
+ message.to_s.match?(/(not found in PATH|command not found|No such file or directory|is not installed)/i)
415
+ end
416
+
261
417
  def provider_overrides_method?(provider_instance, method_name)
262
418
  provider_instance.method(method_name).owner != Providers::Adapter
263
419
  end
264
420
 
265
- def build_result(name:, status:, message:, start_time:)
421
+ def build_result(name:, status:, message:, start_time:, error_category: nil, check: nil)
266
422
  latency = ((monotonic_now - start_time) * 1000).round
267
423
  {
268
424
  name: name,
269
425
  status: status,
270
426
  message: message,
271
- latency_ms: latency
427
+ latency_ms: latency,
428
+ error_category: error_category,
429
+ check: check
272
430
  }
273
431
  end
274
432
 
275
- def build_provider(provider_name, klass)
433
+ def build_provider(provider_name, klass, executor:)
276
434
  config = AgentHarness.configuration.providers[provider_name]
277
435
  klass.new(
278
436
  config: config,
279
- executor: AgentHarness.configuration.command_executor,
437
+ executor: executor || AgentHarness.configuration.command_executor,
280
438
  logger: AgentHarness.logger
281
439
  )
282
440
  end
@@ -25,15 +25,16 @@ module AgentHarness
25
25
  # }
26
26
  # )
27
27
  class ProviderRuntime
28
- attr_reader :model, :base_url, :api_provider, :env, :flags, :metadata
28
+ attr_reader :model, :base_url, :api_provider, :env, :flags, :metadata, :unset_env
29
29
 
30
30
  # @param model [String, nil] model identifier override
31
31
  # @param base_url [String, nil] upstream API base URL override
32
32
  # @param api_provider [String, nil] API-compatible backend name
33
33
  # @param env [Hash<String,String>] extra environment variables for the subprocess
34
34
  # @param flags [Array<String>] extra CLI flags to append
35
+ # @param unset_env [Array<String>] environment variable names to remove from inherited env
35
36
  # @param metadata [Hash] arbitrary provider-specific data
36
- def initialize(model: nil, base_url: nil, api_provider: nil, env: {}, flags: [], metadata: {})
37
+ def initialize(model: nil, base_url: nil, api_provider: nil, env: {}, flags: [], unset_env: [], metadata: {})
37
38
  @model = model
38
39
  @base_url = base_url
39
40
  @api_provider = api_provider
@@ -70,6 +71,21 @@ module AgentHarness
70
71
  end
71
72
  @metadata = metadata_hash.dup.freeze
72
73
 
74
+ # Unset environment variables for the request. These are variable names that
75
+ # should be removed from the inherited environment before the provider
76
+ # command runs.
77
+ unset_array = unset_env || []
78
+ unless unset_array.is_a?(Array)
79
+ raise ArgumentError, "unset_env must be an Array (got #{unset_array.class})"
80
+ end
81
+ normalized_unset_env = unset_array.map.with_index do |key, index|
82
+ key.to_s
83
+ rescue NoMethodError
84
+ raise ArgumentError,
85
+ "unset_env must contain values convertible to String; invalid element at index #{index}: #{key.inspect} (#{key.class})"
86
+ end
87
+ @unset_env = normalized_unset_env.freeze
88
+
73
89
  freeze
74
90
  end
75
91
 
@@ -86,6 +102,7 @@ module AgentHarness
86
102
  api_provider: hash[:api_provider] || hash["api_provider"],
87
103
  env: hash[:env] || hash["env"] || {},
88
104
  flags: hash[:flags] || hash["flags"] || [],
105
+ unset_env: hash[:unset_env] || hash["unset_env"] || [],
89
106
  metadata: hash[:metadata] || hash["metadata"] || {}
90
107
  )
91
108
  end
@@ -109,7 +126,7 @@ module AgentHarness
109
126
  # @return [Boolean]
110
127
  def empty?
111
128
  model.nil? && base_url.nil? && api_provider.nil? &&
112
- env.empty? && flags.empty? && metadata.empty?
129
+ env.empty? && flags.empty? && metadata.empty? && unset_env.empty?
113
130
  end
114
131
  end
115
132
  end
@@ -43,6 +43,17 @@ module AgentHarness
43
43
  raise NotImplementedError, "#{self} must implement .binary_name"
44
44
  end
45
45
 
46
+ # Installation contract for the provider CLI.
47
+ #
48
+ # Downstream applications can use this metadata to install a provider's
49
+ # supported CLI without hardcoding package names, install flags, or
50
+ # version pins outside AgentHarness.
51
+ #
52
+ # @return [Hash, nil] installation metadata or nil when not provided
53
+ def install_contract(version: nil)
54
+ nil
55
+ end
56
+
46
57
  # Required domains for firewall configuration
47
58
  #
48
59
  # @return [Hash] with :domains and :ip_ranges arrays
@@ -63,6 +74,49 @@ module AgentHarness
63
74
  def discover_models
64
75
  []
65
76
  end
77
+
78
+ # Installation contract for this provider's CLI.
79
+ #
80
+ # Downstream apps can use this metadata to provision the provider CLI
81
+ # without hardcoding package names, versions, or binary expectations
82
+ # outside agent-harness.
83
+ #
84
+ # @return [Hash, nil] install metadata, or nil when no first-class
85
+ # installation contract is defined for the provider
86
+ def installation_contract(**options)
87
+ return install_contract unless options.key?(:version)
88
+
89
+ install_contract(version: options[:version])
90
+ end
91
+
92
+ # Build the install command from the provider installation contract.
93
+ #
94
+ # @param version [String, nil] optional explicit version override
95
+ # @return [Array<String>, nil] install command argv or nil when the
96
+ # provider has no install contract
97
+ def install_command(version: nil)
98
+ contract = installation_contract
99
+ return nil unless contract
100
+
101
+ return contract[:install_command] unless version
102
+
103
+ package_name = contract[:package_name]
104
+ unless package_name
105
+ raise ArgumentError, "installation_contract must define :package_name when overriding version"
106
+ end
107
+
108
+ Array(contract[:install_command_prefix]) + ["#{package_name}@#{version}"]
109
+ end
110
+
111
+ # Canonical smoke-test contract for this provider.
112
+ #
113
+ # CLI-backed providers should expose a minimal real-execution prompt so
114
+ # downstream apps can reuse a stable provider-owned health check.
115
+ #
116
+ # @return [Hash, nil] smoke-test metadata or nil when not provided
117
+ def smoke_test_contract
118
+ nil
119
+ end
66
120
  end
67
121
 
68
122
  # Instance methods
@@ -240,6 +294,71 @@ module AgentHarness
240
294
  {healthy: true, message: "OK"}
241
295
  end
242
296
 
297
+ # Canonical smoke-test contract for this provider instance.
298
+ #
299
+ # @return [Hash, nil] smoke-test metadata
300
+ def smoke_test_contract
301
+ self.class.smoke_test_contract if self.class.respond_to?(:smoke_test_contract)
302
+ end
303
+
304
+ # Execute a minimal provider-owned smoke test via the configured executor.
305
+ #
306
+ # @param timeout [Integer, nil] timeout override in seconds
307
+ # @param provider_runtime [ProviderRuntime, Hash, nil] runtime overrides
308
+ # @return [Hash] normalized smoke-test result
309
+ def smoke_test(timeout: nil, provider_runtime: nil)
310
+ contract = smoke_test_contract
311
+ raise NotImplementedError, "#{self.class} does not implement #smoke_test_contract" unless contract
312
+
313
+ prompt = contract[:prompt]
314
+ if !prompt.is_a?(String) || prompt.strip.empty?
315
+ raise ConfigurationError, "#{self.class}.smoke_test_contract must define a non-empty :prompt"
316
+ end
317
+
318
+ response = send_message(
319
+ prompt: prompt,
320
+ timeout: timeout || contract[:timeout],
321
+ provider_runtime: provider_runtime
322
+ )
323
+
324
+ output = response.output.to_s.strip
325
+ expected_output = contract[:expected_output]&.strip
326
+ success = response.success? && (!contract.fetch(:require_output, true) || !output.empty?)
327
+ success &&= expected_output.nil? || output == expected_output
328
+
329
+ if success
330
+ return {
331
+ ok: true,
332
+ status: "ok",
333
+ message: contract[:success_message] || "Smoke test passed",
334
+ error_category: nil,
335
+ output: output,
336
+ exit_code: response.exit_code
337
+ }
338
+ end
339
+
340
+ message = response.error.to_s.strip
341
+ message = output if message.empty?
342
+ message = "Smoke test failed with exit code #{response.exit_code}" if message.empty?
343
+
344
+ {
345
+ ok: false,
346
+ status: "error",
347
+ message: message,
348
+ error_category: classify_smoke_test_message(message),
349
+ output: output,
350
+ exit_code: response.exit_code
351
+ }
352
+ rescue TimeoutError => e
353
+ failure_smoke_test_result(e.message, :timeout)
354
+ rescue AuthenticationError => e
355
+ failure_smoke_test_result(e.message, :auth_expired)
356
+ rescue RateLimitError => e
357
+ failure_smoke_test_result(e.message, :rate_limited)
358
+ rescue ProviderError => e
359
+ failure_smoke_test_result(e.message, classify_smoke_test_message(e.message))
360
+ end
361
+
243
362
  # Execution semantics for this provider
244
363
  #
245
364
  # Returns a hash describing provider-specific execution behavior so
@@ -271,6 +390,23 @@ module AgentHarness
271
390
  def parse_rate_limit_reset(output)
272
391
  nil
273
392
  end
393
+
394
+ private
395
+
396
+ def classify_smoke_test_message(message)
397
+ ErrorTaxonomy.classify(StandardError.new(message.to_s), error_patterns)
398
+ end
399
+
400
+ def failure_smoke_test_result(message, error_category)
401
+ {
402
+ ok: false,
403
+ status: "error",
404
+ message: message,
405
+ error_category: error_category,
406
+ output: nil,
407
+ exit_code: nil
408
+ }
409
+ end
274
410
  end
275
411
  end
276
412
  end
@@ -49,6 +49,10 @@ module AgentHarness
49
49
  {name: "claude-3-5-sonnet", family: "claude-3-5-sonnet", tier: "standard", provider: "aider"}
50
50
  ]
51
51
  end
52
+
53
+ def smoke_test_contract
54
+ Base::DEFAULT_SMOKE_TEST_CONTRACT
55
+ end
52
56
  end
53
57
 
54
58
  def name
@@ -81,6 +81,10 @@ module AgentHarness
81
81
  MODEL_PATTERN.match?(family_name)
82
82
  end
83
83
 
84
+ def smoke_test_contract
85
+ Base::DEFAULT_SMOKE_TEST_CONTRACT
86
+ end
87
+
84
88
  private
85
89
 
86
90
  def parse_models_list(output)