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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2335caa6b19fc12579028ec7c9796c89e588629c7e076b957cac4c6b563d9fdb
4
- data.tar.gz: f7bbe39c811548d1f92262ece7d696d9e8486a93c3d494c7cf397c294f41901e
3
+ metadata.gz: 93f5a11872f464b3eb9834c849f076d9f393f9e1174bc9824952f4c217c9278f
4
+ data.tar.gz: 25bbd835fd6739f84597d0d5653443ecd238b0e89f20f5e0b20029cb5d8bb119
5
5
  SHA512:
6
- metadata.gz: e46f340daf52837fa0ae966b5bb2ce1865338b5b9d20d5901b380696e9fac91f35471ec1ef1512f08cc3305346185dfbfd20c2e4a05c2f7ac7d1b466e9dbc5f5
7
- data.tar.gz: f6e5c23023174cabf7609b5bf61bc8d07e0a527fa6cc2b219eb19265c1f917897cd846360513c9819a33b970e4180443f0a98a9bc02746bf5c57bafc5ba16b1c
6
+ metadata.gz: '081955132b518b5d94498bfeb54ddd7641f1d6968fcb53debf143f7762a666edd40dde6a1369553a42c0dc4985cbff9cb41d7ade8dd727fa80572e7a7293cd2f'
7
+ data.tar.gz: 0345fc32a87b27ba001ae0a6cd26c5b1cc2765f64d7d81bad025b461182cdb0a814c8cdd031478b4e7dcf8bbe1f6b2638e3814cbf3c9537e038dc8bca1255a1f
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.5.7"
2
+ ".": "0.5.8"
3
3
  }
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 Built-in Providers**: Claude Code, Cursor, Gemini CLI, GitHub Copilot, Codex, Aider, OpenCode, Kilocode
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
- # Construct the provider with config/executor/logger to match
98
- # ProviderManager#create_provider and support custom providers
99
- # that may rely on these initializer arguments.
100
- config = AgentHarness.configuration.providers[provider_name]
101
- klass.new(
102
- config: config,
103
- executor: AgentHarness.configuration.command_executor,
104
- logger: AgentHarness.logger
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
- config = @config.providers[name]
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
- klass.new(
211
- config: config,
212
- executor: executor,
213
- logger: AgentHarness.logger
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
- config = AgentHarness.configuration.providers[provider_name]
435
- klass.new(
436
- config: config,
437
- executor: executor || AgentHarness.configuration.command_executor,
438
- logger: AgentHarness.logger
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