agent-harness 0.5.7 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2335caa6b19fc12579028ec7c9796c89e588629c7e076b957cac4c6b563d9fdb
4
- data.tar.gz: f7bbe39c811548d1f92262ece7d696d9e8486a93c3d494c7cf397c294f41901e
3
+ metadata.gz: f476f6224ed25cc79c7bc9c8fb239c1df9f787cd59ff279b8c509b7d515d2727
4
+ data.tar.gz: c0faee9c6d5c139db019707b9174d2d25a4a2fc4a85684ce55eb7baede9fd3be
5
5
  SHA512:
6
- metadata.gz: e46f340daf52837fa0ae966b5bb2ce1865338b5b9d20d5901b380696e9fac91f35471ec1ef1512f08cc3305346185dfbfd20c2e4a05c2f7ac7d1b466e9dbc5f5
7
- data.tar.gz: f6e5c23023174cabf7609b5bf61bc8d07e0a527fa6cc2b219eb19265c1f917897cd846360513c9819a33b970e4180443f0a98a9bc02746bf5c57bafc5ba16b1c
6
+ metadata.gz: 113c0d8afbf5e77c6abcb1f78f0793646b72137e48a9776bfd951ee98c5412d3606628ea19ce4b105a6d8c05d1143a12a5607e1bad82462c629f6bdf3907985b
7
+ data.tar.gz: 4cc2f012c75314a2d096147e4bae9afbed565181878a907339410f7cfdb3ea0cff03e96704a6d3d93cd4f3825eb9f21a97d5351b852d72ec4e09b21a3359df68
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.5.7"
2
+ ".": "0.5.9"
3
3
  }
@@ -0,0 +1,111 @@
1
+ # PR Review Audit Disposition (Issue #91)
2
+
3
+ Audit of PRs merged before review completion in `viamin/agent-harness`.
4
+
5
+ ## P1 PRs
6
+
7
+ ### PR #55 — Per-request provider runtime overrides
8
+
9
+ | Finding | Disposition |
10
+ | ------- | ----------- |
11
+ | env stringifies only keys, not values | Fixed in PR #55 itself |
12
+ | Token tracking uses config model instead of runtime model | Fixed in PR #55 itself |
13
+ | nil env/metadata raises NoMethodError | Fixed in PR #55 itself |
14
+ | flags values not validated as Strings | Fixed in PR #55 itself |
15
+ | Adapter Hash-to-ProviderRuntime docs misleading | Fixed in PR #55 itself |
16
+ | `Array(flags)` freezes caller's array | Fixed in PR #55 itself |
17
+ | freezes caller-provided metadata Hash | Fixed in PR #55 itself |
18
+ | env type validation missing | Fixed in PR #55 itself |
19
+ | `.from_hash` no input type check | Fixed in PR #55 itself |
20
+ | flags accepts single String silently | Fixed in PR #55 itself |
21
+ | Cursor never normalizes/uses provider_runtime | Fixed in PR #55 itself |
22
+ | Cursor reimplements build_env instead of calling super | Fixed in PR #55 itself |
23
+ | runtime.model CLI vs telemetry mismatch | Fixed in PR #55 itself |
24
+ | Missing precedence spec | Fixed in PR #55 itself |
25
+ | `.wrap` kwargs bug on Ruby >= 3.2 | Fixed in PR #55 itself |
26
+ | runtime.model precedence unclear | Fixed in PR #55 itself |
27
+ | **base_url/model/api_provider not type-validated** | **Fixed forward in this PR** |
28
+ | **`.from_hash` treats `false` same as missing** | **Fixed forward in this PR** (hash_value helper) |
29
+ | Cursor not mentioned in PR description | Accepted (docs-only, no code impact) |
30
+
31
+ ### PR #81 — Kilocode CLI installation contract
32
+
33
+ | Finding | Disposition |
34
+ | ------- | ----------- |
35
+ | `installation_contract` can raise NoMethodError for non-Adapter providers | Fixed in PR #81 itself |
36
+ | `provider_installation_contract` doesn't forward `version:` | Fixed in PR #81 itself |
37
+ | Default `installation_contract` doesn't accept `**options` | Fixed in PR #81 itself |
38
+ | Registry forwards `**options` unconditionally | Fixed in PR #81 itself |
39
+ | **`install_command` doesn't handle `version: nil`** | Accepted (produces clear error message via Gem::Version coercion) |
40
+ | **`validate_install_version!` gives generic error for bad input** | **Fixed forward in this PR** |
41
+
42
+ ### PR #57 — Provider configuration capabilities
43
+
44
+ | Finding | Disposition |
45
+ | ------- | ----------- |
46
+ | All 19 findings (schema field removal, auth_modes derivation, model hints, accepts_arbitrary alignment) | All fixed in PR #57 itself across 5 review rounds |
47
+
48
+ ### PR #42 — Gemini and Codex health/auth checks
49
+
50
+ | Finding | Disposition |
51
+ | ------- | ----------- |
52
+ | Temp dir from Dir.mktmpdir never cleaned up | Fixed in PR #42 itself |
53
+ | Config-file API key not validated for sk- prefix | Fixed in PR #42 itself |
54
+ | Error message hardcodes ~/.codex/config.json | Fixed in PR #42 itself |
55
+ | default_flags assumed to be Array, no guard | Fixed in PR #42 itself |
56
+ | Error message only mentions GEMINI_API_KEY | Fixed in PR #42 itself |
57
+ | validate_config doesn't check model/flags types | Fixed in PR #42 itself |
58
+ | JSON parse error leaks credential file contents | Fixed in PR #42 itself |
59
+ | Blank GEMINI_API_KEY doesn't fall back | Fixed in PR #42 itself |
60
+ | validate_config validates default_flags but build_command never uses them | Fixed in PR #42 itself |
61
+ | build_command raises if default_flags is non-Array | Fixed in PR #42 itself |
62
+ | auth_status assumes parsed JSON is Hash | Fixed in PR #42 itself |
63
+ | **auth_status returns inconsistent hash shape (missing auth_method)** | **Fixed forward in this PR** (both Codex and Gemini) |
64
+ | **Numeric status-code regexes can match unrelated numbers** | **Fixed forward in this PR** (added `\b` word boundaries) |
65
+
66
+ ## P2 PRs
67
+
68
+ ### PR #16 — DockerCommandExecutor
69
+
70
+ | Finding | Disposition |
71
+ | ------- | ----------- |
72
+ | Duplicated `normalize_command` method | Already resolved (inherits from protected parent) |
73
+ | `which` missing timeout | Already resolved (timeout: 5 added) |
74
+ | **container_id not validated for whitespace-only** | **Fixed forward in this PR** |
75
+ | Test stubs don't assert command arguments | Accepted (test quality, not a runtime defect) |
76
+
77
+ ### PR #83 — OpenCode CLI installation contract
78
+
79
+ | Finding | Disposition |
80
+ | ------- | ----------- |
81
+ | No unresolved review comments | No action needed |
82
+
83
+ ### PR #80 — Gemini CLI installation contract
84
+
85
+ | Finding | Disposition |
86
+ | ------- | ----------- |
87
+ | `install_contract` signature mismatch between base adapter and callers | Already resolved in current adapter.rb |
88
+ | No guard for providers without `install_contract` | Already resolved in registry.rb |
89
+ | **Malformed version strings not handled gracefully** | Already resolved (Gemini rescues ArgumentError from Gem::Version) |
90
+ | Missing test coverage for edge cases | Accepted (test quality, not a runtime defect) |
91
+
92
+ ### PR #1 — Initial extraction
93
+
94
+ | Finding | Disposition |
95
+ | ------- | ----------- |
96
+ | `timecop` gem not declared in Gemfile | Already resolved (timecop removed from codebase) |
97
+ | Missing YamlLoader/EnvLoader files | Already resolved (references removed) |
98
+ | README binary name for Cursor incorrect | Already resolved (README says cursor-agent, matches code) |
99
+ | README binary name for GitHub Copilot incorrect | Already resolved (README updated) |
100
+ | bin/ excluded from gemspec | Accepted (intentional for gem packaging) |
101
+
102
+ ## Summary of fixes in this PR
103
+
104
+ 1. **ProviderRuntime**: Added type validation for `model`, `base_url`, `api_provider` (must be String or nil)
105
+ 2. **ProviderRuntime**: Replaced `||`-based hash key lookup in `.from_hash` with `hash_value` helper that distinguishes nil from missing keys
106
+ 3. **Codex auth_status**: Added consistent `auth_method` key to all return paths
107
+ 4. **Gemini auth_status**: Added consistent `auth_method` key to all return paths
108
+ 5. **Codex error_patterns**: Added `\b` word boundaries to `/401/` regex
109
+ 6. **Gemini error_patterns**: Added `\b` word boundaries to `/429/` and `/503/` regexes
110
+ 7. **Kilocode**: `validate_install_version!` now rescues malformed version strings with a provider-specific error message
111
+ 8. **DockerCommandExecutor**: Validates whitespace-only `container_id` values
data/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.5.9](https://github.com/viamin/agent-harness/compare/agent-harness/v0.5.8...agent-harness/v0.5.9) (2026-04-12)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * 91: audit and fix-forward defects from PRs merged before review completion ([ec0af12](https://github.com/viamin/agent-harness/commit/ec0af12728f6524b651ebc9b552c477059a2adac))
9
+ * **providers:** close path traversal bypass in env-var-prefixed preparation paths ([493bf55](https://github.com/viamin/agent-harness/commit/493bf55be088a8bde39d143791f3f9b0c90d9c0c))
10
+ * **providers:** harden preparation path validation against env value traversal and command substitution ([ed4b92b](https://github.com/viamin/agent-harness/commit/ed4b92b8760ec4dfa1f50d69b0736563140905b4))
11
+ * **providers:** harden preparation path validation against injection and traversal ([9cb0faf](https://github.com/viamin/agent-harness/commit/9cb0fafbf63c908c7ef7b465bf541ceb5dbf58bb))
12
+ * **providers:** preserve docker idle timeouts ([adde6b4](https://github.com/viamin/agent-harness/commit/adde6b4491023c8baf37249147770cddeff1b0ee))
13
+
14
+ ## [0.5.8](https://github.com/viamin/agent-harness/compare/agent-harness/v0.5.7...agent-harness/v0.5.8) (2026-04-07)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * 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))
20
+ * 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))
21
+ * 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))
22
+ * 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))
23
+
3
24
  ## [0.5.7](https://github.com/viamin/agent-harness/compare/agent-harness/v0.5.6...agent-harness/v0.5.7) (2026-04-07)
4
25
 
5
26
 
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