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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 93f5a11872f464b3eb9834c849f076d9f393f9e1174bc9824952f4c217c9278f
|
|
4
|
+
data.tar.gz: 25bbd835fd6739f84597d0d5653443ecd238b0e89f20f5e0b20029cb5d8bb119
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '081955132b518b5d94498bfeb54ddd7641f1d6968fcb53debf143f7762a666edd40dde6a1369553a42c0dc4985cbff9cb41d7ade8dd727fa80572e7a7293cd2f'
|
|
7
|
+
data.tar.gz: 0345fc32a87b27ba001ae0a6cd26c5b1cc2765f64d7d81bad025b461182cdb0a814c8cdd031478b4e7dcf8bbe1f6b2638e3814cbf3c9537e038dc8bca1255a1f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.5.8](https://github.com/viamin/agent-harness/compare/agent-harness/v0.5.7...agent-harness/v0.5.8) (2026-04-07)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* 64: feat(installers): make Claude CLI installation/version support a first-class provider contract ([#78](https://github.com/viamin/agent-harness/issues/78)) ([0d83590](https://github.com/viamin/agent-harness/commit/0d83590dc9bb9bc7c8d621660bcd73b8eb613d43))
|
|
9
|
+
* 70: feat(installers): make Cursor agent CLI installation/version support a first-class provider contract ([#82](https://github.com/viamin/agent-harness/issues/82)) ([483e9be](https://github.com/viamin/agent-harness/commit/483e9be752de9548020135a86170d978d2a23ae8))
|
|
10
|
+
* 71: feat(installers): make Aider CLI installation/version support a first-class provider contract ([#84](https://github.com/viamin/agent-harness/issues/84)) ([3cfcc1c](https://github.com/viamin/agent-harness/commit/3cfcc1cac831060c12fd8a39130ee7bb9b048aa5))
|
|
11
|
+
* 74: feat(providers): expose provider capability/installability/auth metadata for downstream apps ([#88](https://github.com/viamin/agent-harness/issues/88)) ([4f9b3ba](https://github.com/viamin/agent-harness/commit/4f9b3ba6a53c830eaf53e9233e8df38970c52c8c))
|
|
12
|
+
|
|
3
13
|
## [0.5.7](https://github.com/viamin/agent-harness/compare/agent-harness/v0.5.6...agent-harness/v0.5.7) (2026-04-07)
|
|
4
14
|
|
|
5
15
|
|
data/README.md
CHANGED
|
@@ -5,7 +5,7 @@ A unified Ruby interface for CLI-based AI coding agents like Claude Code, Cursor
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- **Unified Interface**: Single API for multiple AI coding agents
|
|
8
|
-
- **
|
|
8
|
+
- **9 Built-in Providers**: Claude Code, Cursor, Gemini CLI, GitHub Copilot, Codex, Aider, OpenCode, Kilocode, Mistral Vibe
|
|
9
9
|
- **Full Orchestration**: Provider switching, circuit breakers, rate limiting, and health monitoring
|
|
10
10
|
- **Flexible Configuration**: YAML, Ruby DSL, or environment variables
|
|
11
11
|
- **Token Tracking**: Monitor usage across providers for cost and limit management
|
|
@@ -107,6 +107,7 @@ end
|
|
|
107
107
|
| `:aider` | `aider` | Aider coding assistant |
|
|
108
108
|
| `:opencode` | `opencode` | OpenCode CLI |
|
|
109
109
|
| `:kilocode` | `kilo` | Kilocode CLI |
|
|
110
|
+
| `:mistral_vibe` | `mistral-vibe` | Mistral Vibe CLI |
|
|
110
111
|
|
|
111
112
|
### Provider Install Contracts
|
|
112
113
|
|
|
@@ -126,6 +127,41 @@ contract[:install_command]
|
|
|
126
127
|
# => ["npm", "install", "-g", "--ignore-scripts", "@google/gemini-cli@0.35.3"]
|
|
127
128
|
```
|
|
128
129
|
|
|
130
|
+
Cursor also exposes a first-class install contract for container/image builds.
|
|
131
|
+
The contract publishes checksums for both the installer script and the default
|
|
132
|
+
Linux x64 artifact so consumers can verify downloads independently:
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
cursor_install = AgentHarness::Providers::Cursor.install_metadata
|
|
136
|
+
|
|
137
|
+
cursor_install
|
|
138
|
+
# => {
|
|
139
|
+
# source: {
|
|
140
|
+
# type: :shell_script,
|
|
141
|
+
# url: "https://cursor.com/install",
|
|
142
|
+
# resolved_version: "2026.03.30-a5d3e17",
|
|
143
|
+
# default_artifact_url: "https://downloads.cursor.com/lab/2026.03.30-a5d3e17/linux/x64/agent-cli-package.tar.gz"
|
|
144
|
+
# },
|
|
145
|
+
# checksum: {
|
|
146
|
+
# strategy: :sha256,
|
|
147
|
+
# targets: {
|
|
148
|
+
# script: { url: "https://cursor.com/install", value: "8371..." },
|
|
149
|
+
# artifacts: { "linux/x64" => { url: "https://downloads.cursor.com/...", value: "e0d4..." } }
|
|
150
|
+
# }
|
|
151
|
+
# },
|
|
152
|
+
# binary: {
|
|
153
|
+
# name: "cursor-agent",
|
|
154
|
+
# path: "$HOME/.local/bin/cursor-agent",
|
|
155
|
+
# suggested_global_path: "/usr/local/bin/cursor-agent"
|
|
156
|
+
# },
|
|
157
|
+
# version: {
|
|
158
|
+
# default: "latest",
|
|
159
|
+
# supported: "latest",
|
|
160
|
+
# command: ["cursor-agent", "--version"]
|
|
161
|
+
# }
|
|
162
|
+
# }
|
|
163
|
+
```
|
|
164
|
+
|
|
129
165
|
### Direct Provider Access
|
|
130
166
|
|
|
131
167
|
```ruby
|
|
@@ -138,11 +174,28 @@ if AgentHarness::Providers::Registry.instance.get(:claude).available?
|
|
|
138
174
|
puts "Claude CLI is installed"
|
|
139
175
|
end
|
|
140
176
|
|
|
177
|
+
# Ask the harness which Claude CLI install contract it supports
|
|
178
|
+
contract = AgentHarness.install_contract(:claude)
|
|
179
|
+
puts contract[:install][:command]
|
|
180
|
+
# => "tmp_script=$(mktemp) && ... && bash \"$tmp_script\" 2.1.92"
|
|
181
|
+
puts contract[:install][:post_install_binary_path]
|
|
182
|
+
# => "$HOME/.local/bin/claude"
|
|
183
|
+
puts contract[:supported_versions][:default]
|
|
184
|
+
# => "2.1.92"
|
|
185
|
+
puts contract[:supported_versions][:requirement]
|
|
186
|
+
# => ">= 2.1.92, < 2.2.0"
|
|
187
|
+
|
|
141
188
|
# List all registered providers
|
|
142
189
|
AgentHarness::Providers::Registry.instance.all
|
|
143
|
-
# => [:claude, :cursor, :gemini, :github_copilot, :codex, :opencode, :kilocode, :aider]
|
|
190
|
+
# => [:claude, :cursor, :gemini, :github_copilot, :codex, :opencode, :kilocode, :aider, :mistral_vibe]
|
|
144
191
|
```
|
|
145
192
|
|
|
193
|
+
For Claude, the install contract is the first-class source of truth for:
|
|
194
|
+
|
|
195
|
+
- the official install recipe the current harness release expects
|
|
196
|
+
- the expected binary name and normalized PATH entry that recipe leaves behind
|
|
197
|
+
- the supported Claude CLI version boundary the current harness release validates (`default` plus the compatible version range)
|
|
198
|
+
|
|
146
199
|
### Provider Installation Contracts
|
|
147
200
|
|
|
148
201
|
Downstream apps can ask `agent-harness` for provider-specific CLI install
|
|
@@ -170,6 +223,8 @@ Providers that expose installation contracts can also be queried through the
|
|
|
170
223
|
generic API:
|
|
171
224
|
|
|
172
225
|
```ruby
|
|
226
|
+
codex_install = AgentHarness.installation_contract(:codex)
|
|
227
|
+
aider_install = AgentHarness.installation_contract(:aider)
|
|
173
228
|
opencode_install = AgentHarness.installation_contract(:opencode)
|
|
174
229
|
|
|
175
230
|
opencode_install
|
|
@@ -181,11 +236,94 @@ opencode_install
|
|
|
181
236
|
# binary_name: "opencode",
|
|
182
237
|
# install_command: ["npm", "install", "-g", "--ignore-scripts", "opencode-ai@1.3.2"]
|
|
183
238
|
# }
|
|
239
|
+
|
|
240
|
+
aider_install
|
|
241
|
+
# => {
|
|
242
|
+
# source: :uv_tool,
|
|
243
|
+
# bootstrap_package: "uv==0.8.17",
|
|
244
|
+
# package_name: "aider-chat",
|
|
245
|
+
# version: "0.86.2",
|
|
246
|
+
# binary_name: "aider",
|
|
247
|
+
# binary_path: "/usr/local/bin/aider",
|
|
248
|
+
# install_environment: {
|
|
249
|
+
# "UV_TOOL_BIN_DIR" => "/usr/local/bin",
|
|
250
|
+
# "UV_TOOL_DIR" => "/opt/uv/tools",
|
|
251
|
+
# "UV_PYTHON_INSTALL_DIR" => "/opt/uv/python"
|
|
252
|
+
# },
|
|
253
|
+
# bootstrap_commands: [
|
|
254
|
+
# ["python3", "-m", "pip", "install", "--no-cache-dir", "--break-system-packages", "uv==0.8.17"]
|
|
255
|
+
# ],
|
|
256
|
+
# install_command: ["uv", "tool", "install", "--force", "--python", "python3.12", "--with", "pip", "aider-chat==0.86.2"]
|
|
257
|
+
# }
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
For supported providers like Codex and Aider, the install contract tracks
|
|
261
|
+
the CLI version supported by the current `agent-harness` release. The
|
|
262
|
+
contract includes bootstrap requirements, install command shape, and the
|
|
263
|
+
expected runtime binary, and the provider specs assert that runtime
|
|
264
|
+
expectations remain aligned with the published install metadata.
|
|
265
|
+
|
|
266
|
+
### Provider Metadata
|
|
267
|
+
|
|
268
|
+
Downstream apps can also query a consolidated provider metadata contract for
|
|
269
|
+
configuration UIs and policy decisions.
|
|
270
|
+
|
|
271
|
+
The following example shows how to retrieve metadata for the Anthropic provider:
|
|
272
|
+
|
|
273
|
+
```ruby
|
|
274
|
+
metadata = AgentHarness.provider_metadata(:anthropic)
|
|
275
|
+
|
|
276
|
+
metadata
|
|
277
|
+
# => {
|
|
278
|
+
# provider: :claude,
|
|
279
|
+
# canonical_provider: :claude,
|
|
280
|
+
# aliases: [:anthropic],
|
|
281
|
+
# auth: {
|
|
282
|
+
# default_mode: :oauth,
|
|
283
|
+
# supported_modes: [:oauth],
|
|
284
|
+
# service: :anthropic,
|
|
285
|
+
# api_family: :anthropic
|
|
286
|
+
# },
|
|
287
|
+
# runtime: {
|
|
288
|
+
# interface: :cli,
|
|
289
|
+
# requires_cli: true,
|
|
290
|
+
# installable: false,
|
|
291
|
+
# installation: nil,
|
|
292
|
+
# supports_mcp: true,
|
|
293
|
+
# supports_dangerous_mode: true
|
|
294
|
+
# },
|
|
295
|
+
# health_check: {
|
|
296
|
+
# supports_registry_checks: true,
|
|
297
|
+
# auth_check_supported: true,
|
|
298
|
+
# lightweight: true
|
|
299
|
+
# },
|
|
300
|
+
# identity: {
|
|
301
|
+
# bot_usernames: ["claude", "anthropic"]
|
|
302
|
+
# }
|
|
303
|
+
# }
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
To enumerate the full catalog:
|
|
307
|
+
|
|
308
|
+
```ruby
|
|
309
|
+
AgentHarness.provider_metadata_catalog
|
|
310
|
+
# => { claude: {...}, cursor: {...}, gemini: {...}, ... }
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Provider metadata is cached so repeated catalog reads stay cheap.
|
|
314
|
+
Pass `refresh: true` to rebuild metadata and re-run live availability checks when needed:
|
|
315
|
+
|
|
316
|
+
```ruby
|
|
317
|
+
AgentHarness.provider_metadata(:anthropic, refresh: true)
|
|
318
|
+
AgentHarness.provider_metadata_catalog(refresh: true)
|
|
184
319
|
```
|
|
185
320
|
|
|
186
321
|
For providers with install contracts, the metadata tracks the CLI version
|
|
187
322
|
supported by the current `agent-harness` release, and the runtime adapter
|
|
188
323
|
tests assert that the expected binary remains aligned with that contract.
|
|
324
|
+
`runtime[:installation]` is normalized to a stable shape with
|
|
325
|
+
`source_type`, `package_name`, version fields, and install commands so
|
|
326
|
+
downstream apps do not need provider-specific branching.
|
|
189
327
|
|
|
190
328
|
### Custom Providers
|
|
191
329
|
|
|
@@ -94,15 +94,26 @@ module AgentHarness
|
|
|
94
94
|
|
|
95
95
|
def resolve_provider(provider_name)
|
|
96
96
|
klass = Providers::Registry.instance.get(provider_name)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
executor:
|
|
104
|
-
|
|
105
|
-
|
|
97
|
+
canonical_name = Providers::Registry.instance.canonical_name(provider_name)
|
|
98
|
+
config = provider_config_for(provider_name, canonical_name: canonical_name)
|
|
99
|
+
executor = AgentHarness.configuration.command_executor
|
|
100
|
+
logger = AgentHarness.logger
|
|
101
|
+
|
|
102
|
+
provider = if klass.respond_to?(:build_provider_instance, true)
|
|
103
|
+
klass.send(:build_provider_instance, config: config, executor: executor, logger: logger)
|
|
104
|
+
else
|
|
105
|
+
klass.new(config: config, executor: executor, logger: logger)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Ensure the executor is available even when the provider constructor
|
|
109
|
+
# accepts only a subset of keywords (e.g. config: only).
|
|
110
|
+
if provider.respond_to?(:executor=) && provider.executor.nil?
|
|
111
|
+
provider.executor = executor
|
|
112
|
+
elsif !provider.respond_to?(:executor)
|
|
113
|
+
provider.define_singleton_method(:executor) { executor }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
provider
|
|
106
117
|
rescue ConfigurationError
|
|
107
118
|
raise ProviderNotFoundError, "Unknown provider: #{provider_name}"
|
|
108
119
|
end
|
|
@@ -152,6 +163,14 @@ module AgentHarness
|
|
|
152
163
|
"https://claude.ai/oauth/authorize"
|
|
153
164
|
end
|
|
154
165
|
|
|
166
|
+
def provider_config_for(requested_name, canonical_name:)
|
|
167
|
+
requested_key = requested_name.to_sym
|
|
168
|
+
canonical_key = canonical_name.to_sym
|
|
169
|
+
|
|
170
|
+
AgentHarness.configuration.providers[requested_key] ||
|
|
171
|
+
AgentHarness.configuration.providers[canonical_key]
|
|
172
|
+
end
|
|
173
|
+
|
|
155
174
|
def refresh_claude_auth(token: nil)
|
|
156
175
|
raise ArgumentError, "token must be a non-empty string" unless token.is_a?(String) && !token.strip.empty?
|
|
157
176
|
|
|
@@ -205,13 +205,25 @@ module AgentHarness
|
|
|
205
205
|
|
|
206
206
|
def create_provider(name, executor: @config.command_executor)
|
|
207
207
|
klass = @registry.get(name)
|
|
208
|
-
|
|
208
|
+
canonical_name = @registry.canonical_name(name)
|
|
209
|
+
config = provider_config_for(name, canonical_name: canonical_name)
|
|
210
|
+
logger = AgentHarness.logger
|
|
211
|
+
|
|
212
|
+
provider = if klass.respond_to?(:build_provider_instance, true)
|
|
213
|
+
klass.send(:build_provider_instance, config: config, executor: executor, logger: logger)
|
|
214
|
+
else
|
|
215
|
+
klass.new(config: config, executor: executor, logger: logger)
|
|
216
|
+
end
|
|
209
217
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
)
|
|
218
|
+
# Ensure the executor is available even when the provider constructor
|
|
219
|
+
# accepts only a subset of keywords (e.g. config: only).
|
|
220
|
+
if provider.respond_to?(:executor=) && provider.executor.nil?
|
|
221
|
+
provider.executor = executor
|
|
222
|
+
elsif !provider.respond_to?(:executor)
|
|
223
|
+
provider.define_singleton_method(:executor) { executor }
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
provider
|
|
215
227
|
end
|
|
216
228
|
|
|
217
229
|
def select_fallback(provider_name, reason:, executor: nil)
|
|
@@ -243,6 +255,14 @@ module AgentHarness
|
|
|
243
255
|
chain += @config.providers.keys
|
|
244
256
|
chain.uniq
|
|
245
257
|
end
|
|
258
|
+
|
|
259
|
+
def provider_config_for(requested_name, canonical_name:)
|
|
260
|
+
requested_key = requested_name.to_sym
|
|
261
|
+
canonical_key = canonical_name.to_sym
|
|
262
|
+
|
|
263
|
+
@config.providers[requested_key] ||
|
|
264
|
+
@config.providers[canonical_key]
|
|
265
|
+
end
|
|
246
266
|
end
|
|
247
267
|
end
|
|
248
268
|
end
|
|
@@ -431,17 +431,39 @@ module AgentHarness
|
|
|
431
431
|
end
|
|
432
432
|
|
|
433
433
|
def build_provider(provider_name, klass, executor:)
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
)
|
|
434
|
+
canonical_name = Providers::Registry.instance.canonical_name(provider_name)
|
|
435
|
+
config = provider_config_for(provider_name, canonical_name: canonical_name)
|
|
436
|
+
executor ||= AgentHarness.configuration.command_executor
|
|
437
|
+
logger = AgentHarness.logger
|
|
438
|
+
|
|
439
|
+
provider = if klass.respond_to?(:build_provider_instance, true)
|
|
440
|
+
klass.send(:build_provider_instance, config: config, executor: executor, logger: logger)
|
|
441
|
+
else
|
|
442
|
+
klass.new(config: config, executor: executor, logger: logger)
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
# Ensure the executor is available even when the provider constructor
|
|
446
|
+
# accepts only a subset of keywords (e.g. config: only).
|
|
447
|
+
if provider.respond_to?(:executor=) && provider.executor.nil?
|
|
448
|
+
provider.executor = executor
|
|
449
|
+
elsif !provider.respond_to?(:executor)
|
|
450
|
+
provider.define_singleton_method(:executor) { executor }
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
provider
|
|
440
454
|
end
|
|
441
455
|
|
|
442
456
|
def monotonic_now
|
|
443
457
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
444
458
|
end
|
|
459
|
+
|
|
460
|
+
def provider_config_for(requested_name, canonical_name:)
|
|
461
|
+
requested_key = requested_name.to_sym
|
|
462
|
+
canonical_key = canonical_name.to_sym
|
|
463
|
+
|
|
464
|
+
AgentHarness.configuration.providers[requested_key] ||
|
|
465
|
+
AgentHarness.configuration.providers[canonical_key]
|
|
466
|
+
end
|
|
445
467
|
end
|
|
446
468
|
end
|
|
447
469
|
end
|