agent-harness 0.5.7 → 0.5.8
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/CHANGELOG.md +10 -0
- data/README.md +140 -2
- data/lib/agent_harness/authentication.rb +28 -9
- data/lib/agent_harness/orchestration/provider_manager.rb +26 -6
- data/lib/agent_harness/provider_health_check.rb +28 -6
- data/lib/agent_harness/providers/adapter.rb +589 -8
- data/lib/agent_harness/providers/aider.rb +55 -0
- data/lib/agent_harness/providers/anthropic.rb +94 -0
- data/lib/agent_harness/providers/codex.rb +19 -4
- data/lib/agent_harness/providers/cursor.rb +73 -1
- data/lib/agent_harness/providers/gemini.rb +9 -0
- data/lib/agent_harness/providers/github_copilot.rb +12 -0
- data/lib/agent_harness/providers/mistral_vibe.rb +9 -0
- data/lib/agent_harness/providers/opencode.rb +9 -0
- data/lib/agent_harness/providers/registry.rb +392 -18
- data/lib/agent_harness/version.rb +1 -1
- data/lib/agent_harness.rb +28 -0
- metadata +1 -1
|
@@ -17,10 +17,32 @@ module AgentHarness
|
|
|
17
17
|
class Registry
|
|
18
18
|
include Singleton
|
|
19
19
|
|
|
20
|
+
BUILTIN_PROVIDER_DEFINITIONS = [
|
|
21
|
+
{name: :claude, require_path: "agent_harness/providers/anthropic", class_name: :Anthropic, aliases: [:anthropic]},
|
|
22
|
+
{name: :cursor, require_path: "agent_harness/providers/cursor", class_name: :Cursor, aliases: []},
|
|
23
|
+
{name: :gemini, require_path: "agent_harness/providers/gemini", class_name: :Gemini, aliases: []},
|
|
24
|
+
{
|
|
25
|
+
name: :github_copilot,
|
|
26
|
+
require_path: "agent_harness/providers/github_copilot",
|
|
27
|
+
class_name: :GithubCopilot,
|
|
28
|
+
aliases: [:copilot]
|
|
29
|
+
},
|
|
30
|
+
{name: :codex, require_path: "agent_harness/providers/codex", class_name: :Codex, aliases: []},
|
|
31
|
+
{name: :opencode, require_path: "agent_harness/providers/opencode", class_name: :Opencode, aliases: []},
|
|
32
|
+
{name: :kilocode, require_path: "agent_harness/providers/kilocode", class_name: :Kilocode, aliases: []},
|
|
33
|
+
{name: :aider, require_path: "agent_harness/providers/aider", class_name: :Aider, aliases: []},
|
|
34
|
+
{name: :mistral_vibe, require_path: "agent_harness/providers/mistral_vibe", class_name: :MistralVibe, aliases: []}
|
|
35
|
+
].freeze
|
|
36
|
+
|
|
20
37
|
def initialize
|
|
21
38
|
@providers = {}
|
|
22
39
|
@aliases = {}
|
|
40
|
+
@provider_aliases = Hash.new { |hash, key| hash[key] = [] }
|
|
41
|
+
@metadata_runtime_available = {}
|
|
42
|
+
@provider_metadata_cache = {}
|
|
43
|
+
@provider_metadata_catalog_cache = nil
|
|
23
44
|
@builtin_registered = false
|
|
45
|
+
@builtin_registration_in_progress = false
|
|
24
46
|
end
|
|
25
47
|
|
|
26
48
|
# Register a provider class
|
|
@@ -32,11 +54,32 @@ module AgentHarness
|
|
|
32
54
|
def register(name, klass, aliases: [])
|
|
33
55
|
name = name.to_sym
|
|
34
56
|
validate_provider_class!(klass)
|
|
57
|
+
normalized_aliases = aliases
|
|
58
|
+
.filter_map do |alias_name|
|
|
59
|
+
normalized_alias = alias_name.to_s.strip
|
|
60
|
+
next if normalized_alias.empty?
|
|
61
|
+
|
|
62
|
+
normalized_alias.to_sym
|
|
63
|
+
end
|
|
64
|
+
.uniq - [name]
|
|
65
|
+
|
|
66
|
+
validate_provider_name!(name)
|
|
67
|
+
validate_aliases!(name, normalized_aliases)
|
|
68
|
+
unregister_aliases_for(name)
|
|
35
69
|
|
|
36
70
|
@providers[name] = klass
|
|
71
|
+
@provider_aliases[name] = normalized_aliases
|
|
72
|
+
@metadata_runtime_available.delete(name)
|
|
73
|
+
clear_registry_metadata_cache!
|
|
74
|
+
clear_metadata_caches!(klass)
|
|
75
|
+
|
|
76
|
+
normalized_aliases.each do |alias_name|
|
|
77
|
+
previous_owner = @aliases[alias_name]
|
|
78
|
+
if previous_owner && previous_owner != name
|
|
79
|
+
@provider_aliases[previous_owner] = @provider_aliases[previous_owner] - [alias_name]
|
|
80
|
+
end
|
|
37
81
|
|
|
38
|
-
|
|
39
|
-
@aliases[alias_name.to_sym] = name
|
|
82
|
+
@aliases[alias_name] = name
|
|
40
83
|
end
|
|
41
84
|
|
|
42
85
|
AgentHarness.logger&.debug("[AgentHarness::Registry] Registered provider: #{name}")
|
|
@@ -63,6 +106,15 @@ module AgentHarness
|
|
|
63
106
|
@providers.key?(name)
|
|
64
107
|
end
|
|
65
108
|
|
|
109
|
+
# Resolve a provider lookup key to its canonical registered name.
|
|
110
|
+
#
|
|
111
|
+
# @param name [Symbol, String] the provider name or alias
|
|
112
|
+
# @return [Symbol] canonical provider name
|
|
113
|
+
def canonical_name(name)
|
|
114
|
+
ensure_builtin_providers_registered
|
|
115
|
+
resolve_alias(name.to_sym)
|
|
116
|
+
end
|
|
117
|
+
|
|
66
118
|
# List all registered provider names
|
|
67
119
|
#
|
|
68
120
|
# @return [Array<Symbol>] provider names
|
|
@@ -79,6 +131,27 @@ module AgentHarness
|
|
|
79
131
|
@providers.select { |_, klass| klass.available? }.keys
|
|
80
132
|
end
|
|
81
133
|
|
|
134
|
+
# Fetch install contract metadata for a provider.
|
|
135
|
+
#
|
|
136
|
+
# @param name [Symbol, String] the provider name
|
|
137
|
+
# @return [Hash] the provider install contract
|
|
138
|
+
# @raise [ConfigurationError] if the provider does not expose an
|
|
139
|
+
# install contract
|
|
140
|
+
def install_contract(name)
|
|
141
|
+
provider_class = get(name)
|
|
142
|
+
|
|
143
|
+
unless provider_class.respond_to?(:install_contract)
|
|
144
|
+
raise ConfigurationError, "Provider #{provider_class} does not implement .install_contract"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
contract = provider_class.install_contract
|
|
148
|
+
unless contract
|
|
149
|
+
raise ConfigurationError, "Provider #{provider_class} does not expose an install contract"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
contract
|
|
153
|
+
end
|
|
154
|
+
|
|
82
155
|
# Fetch installation metadata for a provider.
|
|
83
156
|
#
|
|
84
157
|
# @param name [Symbol, String] the provider name
|
|
@@ -133,30 +206,315 @@ module AgentHarness
|
|
|
133
206
|
end
|
|
134
207
|
end
|
|
135
208
|
|
|
136
|
-
#
|
|
209
|
+
# Fetch consolidated provider metadata for a provider.
|
|
137
210
|
#
|
|
138
|
-
# @
|
|
211
|
+
# @param name [Symbol, String] the provider name or alias
|
|
212
|
+
# @return [Hash] provider metadata
|
|
213
|
+
# @raise [ConfigurationError] if provider not found
|
|
214
|
+
def provider_metadata(name, refresh: false)
|
|
215
|
+
ensure_builtin_providers_registered
|
|
216
|
+
|
|
217
|
+
requested_name = name.to_sym
|
|
218
|
+
canonical_name = resolve_alias(requested_name)
|
|
219
|
+
cache_key = [requested_name, canonical_name]
|
|
220
|
+
|
|
221
|
+
return duplicate_metadata(@provider_metadata_cache[cache_key]) if !refresh && @provider_metadata_cache.key?(cache_key)
|
|
222
|
+
|
|
223
|
+
refresh_provider_metadata_cache!(requested_name, canonical_name, refresh: refresh)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Get consolidated metadata for all registered providers.
|
|
227
|
+
#
|
|
228
|
+
# @param refresh [Boolean] when true, refresh live runtime metadata such
|
|
229
|
+
# as CLI availability instead of reusing cached values
|
|
230
|
+
# @return [Hash<Symbol, Hash>] provider metadata keyed by canonical provider
|
|
231
|
+
def provider_metadata_catalog(refresh: false)
|
|
232
|
+
ensure_builtin_providers_registered
|
|
233
|
+
|
|
234
|
+
return duplicate_metadata(@provider_metadata_catalog_cache) if !refresh && @provider_metadata_catalog_cache
|
|
235
|
+
|
|
236
|
+
if refresh
|
|
237
|
+
clear_registry_metadata_cache!
|
|
238
|
+
clear_all_auth_status_metadata_caches!
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
catalog = @providers.keys.each_with_object({}) do |name, result|
|
|
242
|
+
result[name] = refresh_provider_metadata_cache!(
|
|
243
|
+
name,
|
|
244
|
+
name,
|
|
245
|
+
refresh: refresh,
|
|
246
|
+
invalidate_provider_cache: false,
|
|
247
|
+
invalidate_catalog: false
|
|
248
|
+
)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
@provider_metadata_catalog_cache = duplicate_metadata(catalog)
|
|
252
|
+
duplicate_metadata(catalog)
|
|
253
|
+
end
|
|
254
|
+
|
|
139
255
|
def reset!
|
|
256
|
+
@providers.each_value { |klass| clear_metadata_caches!(klass) }
|
|
140
257
|
@providers.clear
|
|
141
258
|
@aliases.clear
|
|
259
|
+
@provider_aliases.clear
|
|
260
|
+
@metadata_runtime_available.clear
|
|
261
|
+
clear_registry_metadata_cache!
|
|
142
262
|
@builtin_registered = false
|
|
263
|
+
@builtin_registration_in_progress = false
|
|
143
264
|
end
|
|
144
265
|
|
|
145
266
|
private
|
|
146
267
|
|
|
268
|
+
def clear_registry_metadata_cache!
|
|
269
|
+
@provider_metadata_cache.clear
|
|
270
|
+
@provider_metadata_catalog_cache = nil
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def invalidate_provider_metadata_cache!(canonical_name)
|
|
274
|
+
@provider_metadata_cache.delete_if do |(_, cached_canonical_name), _|
|
|
275
|
+
cached_canonical_name == canonical_name
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def duplicate_metadata(value)
|
|
280
|
+
case value
|
|
281
|
+
when Hash
|
|
282
|
+
value.each_with_object({}) do |(key, nested_value), copy|
|
|
283
|
+
copy[key] = duplicate_metadata(nested_value)
|
|
284
|
+
end
|
|
285
|
+
when Array
|
|
286
|
+
value.map { |nested_value| duplicate_metadata(nested_value) }
|
|
287
|
+
when String
|
|
288
|
+
value.dup
|
|
289
|
+
else
|
|
290
|
+
value
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
147
294
|
def resolve_alias(name)
|
|
148
295
|
@aliases[name] || name
|
|
149
296
|
end
|
|
150
297
|
|
|
298
|
+
def unregister_aliases_for(name)
|
|
299
|
+
previous_aliases = @provider_aliases[name]
|
|
300
|
+
return if previous_aliases.nil? || previous_aliases.empty?
|
|
301
|
+
|
|
302
|
+
previous_aliases.each do |alias_name|
|
|
303
|
+
@aliases.delete(alias_name)
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def clear_metadata_caches!(klass)
|
|
308
|
+
clear_class_metadata_cache!(klass, :@metadata_runtime_available)
|
|
309
|
+
clear_class_metadata_cache!(klass, :@auth_status_available)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def clear_all_auth_status_metadata_caches!
|
|
313
|
+
@providers.each_value { |klass| clear_class_auth_status_metadata_cache!(klass) }
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def clear_class_auth_status_metadata_cache!(klass, canonical_name = nil)
|
|
317
|
+
return unless klass.instance_variable_defined?(:@auth_status_available)
|
|
318
|
+
|
|
319
|
+
return clear_class_metadata_cache!(klass, :@auth_status_available) unless canonical_name
|
|
320
|
+
|
|
321
|
+
auth_status_cache = klass.instance_variable_get(:@auth_status_available)
|
|
322
|
+
auth_status_cache.delete_if do |(_requested_name, cached_canonical_name), _|
|
|
323
|
+
cached_canonical_name == canonical_name
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def build_provider_metadata(requested_name, canonical_name, refresh:)
|
|
328
|
+
klass = @providers[canonical_name] || raise(ConfigurationError, "Unknown provider: #{canonical_name}")
|
|
329
|
+
aliases = @provider_aliases[canonical_name]
|
|
330
|
+
|
|
331
|
+
if klass.respond_to?(:provider_metadata)
|
|
332
|
+
klass.provider_metadata(
|
|
333
|
+
aliases: aliases,
|
|
334
|
+
refresh: refresh,
|
|
335
|
+
requested_name: requested_name,
|
|
336
|
+
canonical_name: canonical_name
|
|
337
|
+
)
|
|
338
|
+
else
|
|
339
|
+
fallback_provider_metadata(canonical_name, klass, aliases, refresh: refresh)
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def refresh_provider_metadata_cache!(
|
|
344
|
+
requested_name,
|
|
345
|
+
canonical_name,
|
|
346
|
+
refresh:,
|
|
347
|
+
invalidate_provider_cache: refresh,
|
|
348
|
+
invalidate_catalog: true
|
|
349
|
+
)
|
|
350
|
+
cache_key = [requested_name, canonical_name]
|
|
351
|
+
invalidate_provider_metadata_cache!(canonical_name) if invalidate_provider_cache
|
|
352
|
+
klass = @providers[canonical_name]
|
|
353
|
+
clear_class_auth_status_metadata_cache!(klass, canonical_name) if refresh && klass
|
|
354
|
+
@provider_metadata_catalog_cache = nil if refresh && invalidate_catalog
|
|
355
|
+
|
|
356
|
+
metadata = build_provider_metadata(requested_name, canonical_name, refresh: refresh)
|
|
357
|
+
@provider_metadata_cache[cache_key] = duplicate_metadata(metadata)
|
|
358
|
+
duplicate_metadata(metadata)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def clear_class_metadata_cache!(klass, ivar_name)
|
|
362
|
+
return unless klass.instance_variable_defined?(ivar_name)
|
|
363
|
+
|
|
364
|
+
klass.remove_instance_variable(ivar_name)
|
|
365
|
+
end
|
|
366
|
+
|
|
151
367
|
def validate_provider_class!(klass)
|
|
152
368
|
includes_adapter = klass.include?(Adapter)
|
|
153
369
|
has_required_methods = klass.respond_to?(:provider_name) &&
|
|
154
370
|
klass.respond_to?(:available?) &&
|
|
155
371
|
klass.respond_to?(:binary_name)
|
|
156
372
|
|
|
157
|
-
return if includes_adapter
|
|
373
|
+
return if includes_adapter
|
|
374
|
+
return if has_required_methods
|
|
375
|
+
|
|
376
|
+
raise ConfigurationError,
|
|
377
|
+
"Provider class must include AgentHarness::Providers::Adapter or implement required class methods"
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def validate_provider_name!(name)
|
|
381
|
+
# Reject canonical provider names that match a builtin canonical name
|
|
382
|
+
# (e.g. :claude, :gemini). Authentication hardcodes routing for names
|
|
383
|
+
# like :claude/:anthropic to provider-specific OAuth file handling, so
|
|
384
|
+
# registering a non-builtin provider under those names yields incorrect
|
|
385
|
+
# auth behavior at runtime.
|
|
386
|
+
if builtin_provider_name?(name) && !@builtin_registration_in_progress
|
|
387
|
+
raise ConfigurationError,
|
|
388
|
+
"Provider name #{name.inspect} is reserved as a builtin canonical provider"
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# Reject canonical provider names that match a reserved builtin alias
|
|
392
|
+
# (e.g. :anthropic is an alias for :claude).
|
|
393
|
+
builtin_alias_owner = reserved_builtin_alias_owner(name)
|
|
394
|
+
if builtin_alias_owner && !@builtin_registration_in_progress
|
|
395
|
+
raise ConfigurationError,
|
|
396
|
+
"Provider name #{name.inspect} is reserved as a builtin alias for #{builtin_alias_owner.inspect}"
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
conflicting_provider = @aliases[name]
|
|
400
|
+
return unless conflicting_provider && conflicting_provider != name
|
|
401
|
+
|
|
402
|
+
raise ConfigurationError, "Provider #{name.inspect} conflicts with registered alias for #{conflicting_provider.inspect}"
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def validate_aliases!(name, aliases)
|
|
406
|
+
conflicting_alias = aliases.find do |alias_name|
|
|
407
|
+
next false if alias_name == name
|
|
408
|
+
next true if builtin_provider_name?(alias_name)
|
|
409
|
+
|
|
410
|
+
# Reject aliases that match a reserved builtin alias (e.g. :anthropic
|
|
411
|
+
# for :claude) unless the registering provider is the owning builtin.
|
|
412
|
+
# Authentication hardcodes routing for these names, so allowing a
|
|
413
|
+
# custom provider to claim them would cause auth_status/auth_url to
|
|
414
|
+
# hit the wrong provider.
|
|
415
|
+
alias_owner = reserved_builtin_alias_owner(alias_name)
|
|
416
|
+
next true if alias_owner && alias_owner != name
|
|
158
417
|
|
|
159
|
-
|
|
418
|
+
@providers.key?(alias_name) ||
|
|
419
|
+
(@aliases.key?(alias_name) && @aliases[alias_name] != name)
|
|
420
|
+
end
|
|
421
|
+
return unless conflicting_alias
|
|
422
|
+
|
|
423
|
+
builtin_alias_owner = reserved_builtin_alias_owner(conflicting_alias)
|
|
424
|
+
owner = if builtin_alias_owner
|
|
425
|
+
builtin_alias_owner
|
|
426
|
+
elsif @providers.key?(conflicting_alias)
|
|
427
|
+
conflicting_alias
|
|
428
|
+
elsif builtin_provider_name?(conflicting_alias)
|
|
429
|
+
:builtin_provider
|
|
430
|
+
else
|
|
431
|
+
@aliases[conflicting_alias]
|
|
432
|
+
end
|
|
433
|
+
raise ConfigurationError, "Alias #{conflicting_alias.inspect} conflicts with registered provider #{owner.inspect}"
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def builtin_provider_name?(name)
|
|
437
|
+
BUILTIN_PROVIDER_DEFINITIONS.any? { |definition| definition[:name] == name }
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def reserved_builtin_alias_owner(name)
|
|
441
|
+
definition = BUILTIN_PROVIDER_DEFINITIONS.find do |defn|
|
|
442
|
+
defn[:aliases].include?(name) && defn[:name] != name
|
|
443
|
+
end
|
|
444
|
+
definition&.dig(:name)
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def fallback_provider_metadata(name, klass, aliases, refresh: false)
|
|
448
|
+
normalized_aliases = aliases
|
|
449
|
+
.filter_map do |alias_name|
|
|
450
|
+
normalized_alias = alias_name.to_s.strip
|
|
451
|
+
next if normalized_alias.empty?
|
|
452
|
+
|
|
453
|
+
normalized_alias.to_sym
|
|
454
|
+
end
|
|
455
|
+
.uniq
|
|
456
|
+
.reject { |alias_name| alias_name == name }
|
|
457
|
+
installation = if klass.respond_to?(:installation_contract)
|
|
458
|
+
Adapter.normalize_metadata_installation(
|
|
459
|
+
klass.installation_contract,
|
|
460
|
+
provider_name: name,
|
|
461
|
+
binary_name: klass.binary_name
|
|
462
|
+
)
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
{
|
|
466
|
+
provider: name,
|
|
467
|
+
canonical_provider: name,
|
|
468
|
+
aliases: normalized_aliases,
|
|
469
|
+
display_name: name.to_s.split("_").map(&:capitalize).join(" "),
|
|
470
|
+
binary_name: klass.binary_name,
|
|
471
|
+
auth: {
|
|
472
|
+
default_mode: nil,
|
|
473
|
+
supported_modes: [],
|
|
474
|
+
service: nil,
|
|
475
|
+
api_family: nil
|
|
476
|
+
},
|
|
477
|
+
runtime: {
|
|
478
|
+
interface: :cli,
|
|
479
|
+
requires_cli: true,
|
|
480
|
+
available: metadata_runtime_available(name, klass, refresh: refresh),
|
|
481
|
+
installable: !installation.nil?,
|
|
482
|
+
installation: installation,
|
|
483
|
+
prompt_delivery: nil,
|
|
484
|
+
output_format: nil,
|
|
485
|
+
sandbox_aware: nil,
|
|
486
|
+
uses_subcommand: nil,
|
|
487
|
+
supports_mcp: false,
|
|
488
|
+
supported_mcp_transports: [],
|
|
489
|
+
supports_sessions: false,
|
|
490
|
+
supports_dangerous_mode: false
|
|
491
|
+
},
|
|
492
|
+
configuration: {fields: [], auth_modes: [], openai_compatible: false},
|
|
493
|
+
capabilities: {streaming: false, file_upload: false, vision: false, tool_use: false, json_mode: false, mcp: false, dangerous_mode: false},
|
|
494
|
+
health_check: {
|
|
495
|
+
supports_registry_checks: false,
|
|
496
|
+
auth_check_supported: false,
|
|
497
|
+
provider_status: false,
|
|
498
|
+
configuration_validation: false,
|
|
499
|
+
lightweight: false
|
|
500
|
+
},
|
|
501
|
+
identity: {
|
|
502
|
+
bot_usernames: [name, *normalized_aliases]
|
|
503
|
+
.filter_map do |identity|
|
|
504
|
+
normalized_identity = identity.to_s.strip
|
|
505
|
+
normalized_identity unless normalized_identity.empty?
|
|
506
|
+
end
|
|
507
|
+
.uniq
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
def metadata_runtime_available(name, klass, refresh: false)
|
|
513
|
+
if refresh || !@metadata_runtime_available.key?(name)
|
|
514
|
+
@metadata_runtime_available[name] = klass.available?
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
@metadata_runtime_available[name]
|
|
160
518
|
end
|
|
161
519
|
|
|
162
520
|
def ensure_builtin_providers_registered
|
|
@@ -167,26 +525,42 @@ module AgentHarness
|
|
|
167
525
|
end
|
|
168
526
|
|
|
169
527
|
def register_builtin_providers
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
528
|
+
@builtin_registration_in_progress = true
|
|
529
|
+
BUILTIN_PROVIDER_DEFINITIONS.each do |definition|
|
|
530
|
+
definition_name = definition[:name]
|
|
531
|
+
next if builtin_provider_name_taken?(definition_name)
|
|
532
|
+
|
|
533
|
+
register_if_available(
|
|
534
|
+
definition_name,
|
|
535
|
+
definition[:require_path],
|
|
536
|
+
definition[:class_name],
|
|
537
|
+
aliases: definition[:aliases]
|
|
538
|
+
)
|
|
539
|
+
end
|
|
540
|
+
ensure
|
|
541
|
+
@builtin_registration_in_progress = false
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
def builtin_provider_name_taken?(name)
|
|
545
|
+
@providers.key?(name)
|
|
181
546
|
end
|
|
182
547
|
|
|
183
548
|
def register_if_available(name, require_path, class_name, aliases: [])
|
|
184
549
|
require_relative require_path.sub("agent_harness/providers/", "")
|
|
185
550
|
klass = AgentHarness::Providers.const_get(class_name)
|
|
186
|
-
register(name, klass, aliases: aliases)
|
|
551
|
+
register(name, klass, aliases: builtin_aliases_for(name, aliases))
|
|
187
552
|
rescue LoadError, NameError => e
|
|
188
553
|
AgentHarness.logger&.debug("[AgentHarness::Registry] Provider #{name} not available: #{e.message}")
|
|
189
554
|
end
|
|
555
|
+
|
|
556
|
+
def builtin_aliases_for(name, aliases)
|
|
557
|
+
Array(aliases).reject do |alias_name|
|
|
558
|
+
alias_key = alias_name.to_sym
|
|
559
|
+
next false if alias_key == name
|
|
560
|
+
|
|
561
|
+
@providers.key?(alias_key) || @aliases.key?(alias_key)
|
|
562
|
+
end
|
|
563
|
+
end
|
|
190
564
|
end
|
|
191
565
|
end
|
|
192
566
|
end
|
data/lib/agent_harness.rb
CHANGED
|
@@ -84,6 +84,14 @@ module AgentHarness
|
|
|
84
84
|
conductor.provider_manager.get_provider(name)
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
+
# Get install contract metadata for a provider
|
|
88
|
+
# @param name [Symbol, String] the provider name
|
|
89
|
+
# @return [Hash] install contract metadata
|
|
90
|
+
# @raise [ConfigurationError] if the provider does not expose an install contract
|
|
91
|
+
def install_contract(name)
|
|
92
|
+
Providers::Registry.instance.install_contract(name)
|
|
93
|
+
end
|
|
94
|
+
|
|
87
95
|
# Returns install metadata for a provider CLI when the provider exposes it.
|
|
88
96
|
#
|
|
89
97
|
# @param provider_name [Symbol, String] the provider name
|
|
@@ -118,6 +126,26 @@ module AgentHarness
|
|
|
118
126
|
Providers::Registry.instance.installation_contracts
|
|
119
127
|
end
|
|
120
128
|
|
|
129
|
+
# Get consolidated metadata for a provider.
|
|
130
|
+
#
|
|
131
|
+
# @param provider_name [Symbol, String] the provider name or alias
|
|
132
|
+
# @param refresh [Boolean] when true, refresh live runtime metadata such as
|
|
133
|
+
# CLI availability instead of reusing cached values
|
|
134
|
+
# @return [Hash] provider metadata
|
|
135
|
+
# @raise [ConfigurationError] if the provider name is not registered
|
|
136
|
+
def provider_metadata(provider_name, refresh: false)
|
|
137
|
+
Providers::Registry.instance.provider_metadata(provider_name, refresh: refresh)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Get consolidated metadata for all registered providers.
|
|
141
|
+
#
|
|
142
|
+
# @param refresh [Boolean] when true, refresh live runtime metadata such as
|
|
143
|
+
# CLI availability instead of reusing cached values
|
|
144
|
+
# @return [Hash<Symbol, Hash>] provider metadata keyed by canonical provider
|
|
145
|
+
def provider_metadata_catalog(refresh: false)
|
|
146
|
+
Providers::Registry.instance.provider_metadata_catalog(refresh: refresh)
|
|
147
|
+
end
|
|
148
|
+
|
|
121
149
|
# Get smoke-test metadata for a provider CLI when the provider exposes it.
|
|
122
150
|
#
|
|
123
151
|
# @param provider_name [Symbol, String] the provider name
|