agent-harness 0.5.2 → 0.5.3

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: f3ca48a731020ba5b0668cee27ca19c8d7ca8e9cfc907d0f9b4f42d77b56b7a8
4
- data.tar.gz: 69b9c2ce746b4aa1a594479a7295a6510468e1f0d428a02896c218c711889b91
3
+ metadata.gz: 3dfde368468f5c0c037ec1cb2fefd705e813d192e3723fd70126757cd851cf14
4
+ data.tar.gz: f68f11de99ea61807ab1bb7a94a28aca4aa37e3346469b195a77019f6ceeceec
5
5
  SHA512:
6
- metadata.gz: cb329e3f9d7e441d692a7a88ce7ed8c1637f0b314875ae355d5c4c088275fbc77e0726e979069a905c458d05599ea6f90b94434e8a2edcacee7eeb6878b32afc
7
- data.tar.gz: 3b56558191606eb4431b6d9253a4dec6ada80a605af3af7422bd7db2163f1ce61b1b0b4c55c1b2f6dab4e046aa1504afbb717a172491e76f3f6f7d114b97c9f8
6
+ metadata.gz: 4cf0d1807fee47eb2ef1aecc63ab8c10ce71a681d6c98ca2cb860ae7eb9ba7b1a8d88e4382e9ff1f5f2f25acd9380a6cf4c3d8e61f5cf008d920b4d2bbf7b4bc
7
+ data.tar.gz: cd6ac3ae08acf302369a28cdda0a3e332994f679ad37258c0f4dd869f99f18396ccb1cb52d9a6d5bcbb3c6ac3c776ba87a5338edb8280b46785edce254eb8258
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.5.2"
2
+ ".": "0.5.3"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.5.3](https://github.com/viamin/agent-harness/compare/agent-harness/v0.5.2...agent-harness/v0.5.3) (2026-03-27)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * 41: Add provider-specific health/auth checks for Gemini and Codex ([#42](https://github.com/viamin/agent-harness/issues/42)) ([be95135](https://github.com/viamin/agent-harness/commit/be9513534e55aa3df9c0885b6e3580a3b146eb93))
9
+
3
10
  ## [0.5.2](https://github.com/viamin/agent-harness/compare/agent-harness/v0.5.1...agent-harness/v0.5.2) (2026-03-24)
4
11
 
5
12
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
4
+
3
5
  module AgentHarness
4
6
  module Providers
5
7
  # OpenAI Codex CLI provider
@@ -78,11 +80,104 @@ module AgentHarness
78
80
  ["--session", session_id]
79
81
  end
80
82
 
83
+ def error_patterns
84
+ {
85
+ rate_limited: [
86
+ /rate.?limit/i,
87
+ /too.?many.?requests/i,
88
+ /429/
89
+ ],
90
+ auth_expired: [
91
+ /invalid.*api.*key/i,
92
+ /unauthorized/i,
93
+ /authentication/i,
94
+ /401/,
95
+ /incorrect.*api.*key/i
96
+ ],
97
+ quota_exceeded: [
98
+ /quota.*exceeded/i,
99
+ /insufficient.*quota/i,
100
+ /billing/i
101
+ ],
102
+ transient: [
103
+ /timeout/i,
104
+ /connection.*reset/i,
105
+ /service.*unavailable/i,
106
+ /503/,
107
+ /502/
108
+ ]
109
+ }
110
+ end
111
+
112
+ def auth_status
113
+ api_key = ENV["OPENAI_API_KEY"]
114
+ if api_key && !api_key.strip.empty?
115
+ if api_key.strip.start_with?("sk-")
116
+ return {valid: true, expires_at: nil, error: nil, auth_method: :api_key}
117
+ else
118
+ return {valid: false, expires_at: nil, error: "OPENAI_API_KEY is set but does not appear to be a valid OpenAI API key"}
119
+ end
120
+ end
121
+
122
+ credentials = read_codex_credentials
123
+ if credentials
124
+ key = credentials["api_key"] || credentials["apiKey"] || credentials["OPENAI_API_KEY"]
125
+ if key.is_a?(String) && !key.strip.empty?
126
+ if key.strip.start_with?("sk-")
127
+ return {valid: true, expires_at: nil, error: nil, auth_method: :config_file}
128
+ else
129
+ return {valid: false, expires_at: nil, error: "Config file API key is set but does not appear to be a valid OpenAI API key"}
130
+ end
131
+ end
132
+ end
133
+
134
+ {valid: false, expires_at: nil, error: "No OpenAI API key found. Set OPENAI_API_KEY or configure in #{codex_config_path}"}
135
+ rescue IOError, JSON::ParserError => e
136
+ {valid: false, expires_at: nil, error: e.message}
137
+ end
138
+
139
+ def health_status
140
+ unless self.class.available?
141
+ return {healthy: false, message: "Codex CLI not found in PATH. Install from https://github.com/openai/codex"}
142
+ end
143
+
144
+ auth = auth_status
145
+ unless auth[:valid]
146
+ return {healthy: false, message: auth[:error]}
147
+ end
148
+
149
+ {healthy: true, message: "Codex CLI available and authenticated"}
150
+ end
151
+
152
+ def validate_config
153
+ errors = []
154
+
155
+ flags = @config.default_flags
156
+ unless flags.nil?
157
+ if flags.is_a?(Array)
158
+ invalid = flags.reject { |f| f.is_a?(String) }
159
+ errors << "default_flags contains non-string values" if invalid.any?
160
+ else
161
+ errors << "default_flags must be an array of strings"
162
+ end
163
+ end
164
+
165
+ {valid: errors.empty?, errors: errors}
166
+ end
167
+
81
168
  protected
82
169
 
83
170
  def build_command(prompt, options)
84
171
  cmd = [self.class.binary_name, "exec"]
85
172
 
173
+ flags = @config.default_flags
174
+ if flags
175
+ unless flags.is_a?(Array)
176
+ raise ArgumentError, "Codex configuration error: default_flags must be an array of strings"
177
+ end
178
+ cmd += flags if flags.any?
179
+ end
180
+
86
181
  if options[:session]
87
182
  cmd += session_flags(options[:session])
88
183
  end
@@ -95,6 +190,29 @@ module AgentHarness
95
190
  def default_timeout
96
191
  300
97
192
  end
193
+
194
+ private
195
+
196
+ def read_codex_credentials
197
+ path = codex_config_path
198
+ return nil unless File.exist?(path)
199
+
200
+ parsed = JSON.parse(File.read(path))
201
+ return nil unless parsed.is_a?(Hash)
202
+
203
+ parsed
204
+ rescue Errno::ENOENT
205
+ nil
206
+ rescue Errno::EACCES => e
207
+ raise IOError, "Permission denied reading Codex config at #{path}: #{e.message}"
208
+ rescue JSON::ParserError
209
+ raise JSON::ParserError, "Invalid JSON in Codex config at #{path}"
210
+ end
211
+
212
+ def codex_config_path
213
+ config_dir = ENV["CODEX_CONFIG_DIR"] || File.expand_path("~/.codex")
214
+ File.join(config_dir, "config.json")
215
+ end
98
216
  end
99
217
  end
100
218
  end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
4
+ require "time"
5
+
3
6
  module AgentHarness
4
7
  module Providers
5
8
  # Google Gemini CLI provider
@@ -106,7 +109,11 @@ module AgentHarness
106
109
  auth_expired: [
107
110
  /authentication/i,
108
111
  /unauthorized/i,
109
- /invalid.?credentials/i
112
+ /invalid.?credentials/i,
113
+ /login.*required/i,
114
+ /not.*logged.*in/i,
115
+ /credentials.*expired/i,
116
+ /account.*not.*verified/i
110
117
  ],
111
118
  transient: [
112
119
  /timeout/i,
@@ -116,6 +123,68 @@ module AgentHarness
116
123
  }
117
124
  end
118
125
 
126
+ def auth_status
127
+ api_key = [ENV["GEMINI_API_KEY"], ENV["GOOGLE_API_KEY"]].find { |key| key && !key.strip.empty? }
128
+ if api_key
129
+ return {valid: true, expires_at: nil, error: nil, auth_method: :api_key}
130
+ end
131
+
132
+ credentials = read_gemini_credentials
133
+ return {valid: false, expires_at: nil, error: "No Gemini credentials found. Run 'gemini auth login' or set GEMINI_API_KEY or GOOGLE_API_KEY"} unless credentials
134
+
135
+ token = credentials["access_token"] || credentials["oauth_token"]
136
+ unless token.is_a?(String) && !token.strip.empty?
137
+ return {valid: false, expires_at: nil, error: "No authentication token in Gemini credentials"}
138
+ end
139
+
140
+ expires_at = parse_gemini_expiry(credentials)
141
+ if expires_at && expires_at < Time.now
142
+ {valid: false, expires_at: expires_at, error: "Gemini session expired. Run 'gemini auth login' to re-authenticate"}
143
+ else
144
+ {valid: true, expires_at: expires_at, error: nil, auth_method: :oauth}
145
+ end
146
+ rescue IOError, JSON::ParserError => e
147
+ {valid: false, expires_at: nil, error: e.message}
148
+ end
149
+
150
+ def health_status
151
+ unless self.class.available?
152
+ return {healthy: false, message: "Gemini CLI not found in PATH. Install from https://github.com/google-gemini/gemini-cli"}
153
+ end
154
+
155
+ auth = auth_status
156
+ unless auth[:valid]
157
+ return {healthy: false, message: auth[:error]}
158
+ end
159
+
160
+ {healthy: true, message: "Gemini CLI available and authenticated"}
161
+ end
162
+
163
+ def validate_config
164
+ errors = []
165
+
166
+ model = @config.model
167
+ if !model.nil? && !model.is_a?(String)
168
+ errors << "model must be a string"
169
+ elsif model.is_a?(String) && !model.empty?
170
+ unless self.class.supports_model_family?(model)
171
+ errors << "Unrecognized model '#{model}'. Expected a Gemini model (e.g., gemini-2.0-flash, gemini-2.5-pro)"
172
+ end
173
+ end
174
+
175
+ flags = @config.default_flags
176
+ unless flags.nil?
177
+ if flags.is_a?(Array)
178
+ invalid = flags.reject { |f| f.is_a?(String) }
179
+ errors << "default_flags contains non-string values" if invalid.any?
180
+ else
181
+ errors << "default_flags must be an array of strings"
182
+ end
183
+ end
184
+
185
+ {valid: errors.empty?, errors: errors}
186
+ end
187
+
119
188
  protected
120
189
 
121
190
  def build_command(prompt, options)
@@ -125,7 +194,13 @@ module AgentHarness
125
194
  cmd += ["--model", @config.model]
126
195
  end
127
196
 
128
- cmd += @config.default_flags if @config.default_flags&.any?
197
+ flags = @config.default_flags
198
+ if flags
199
+ unless flags.is_a?(Array)
200
+ raise ArgumentError, "Gemini configuration error: default_flags must be an array of strings"
201
+ end
202
+ cmd += flags if flags.any?
203
+ end
129
204
 
130
205
  cmd += ["--prompt", prompt]
131
206
 
@@ -135,6 +210,45 @@ module AgentHarness
135
210
  def default_timeout
136
211
  300
137
212
  end
213
+
214
+ private
215
+
216
+ def read_gemini_credentials
217
+ path = gemini_credentials_path
218
+ return nil unless File.exist?(path)
219
+
220
+ parsed = JSON.parse(File.read(path))
221
+ return nil unless parsed.is_a?(Hash)
222
+
223
+ parsed
224
+ rescue Errno::ENOENT
225
+ nil
226
+ rescue Errno::EACCES => e
227
+ raise IOError, "Permission denied reading Gemini credentials at #{path}: #{e.message}"
228
+ rescue JSON::ParserError
229
+ raise JSON::ParserError, "Invalid JSON in Gemini credentials at #{path}"
230
+ end
231
+
232
+ def gemini_credentials_path
233
+ config_dir = ENV["GEMINI_CONFIG_DIR"] || File.expand_path("~/.gemini")
234
+ File.join(config_dir, "credentials.json")
235
+ end
236
+
237
+ def parse_gemini_expiry(credentials)
238
+ value = credentials["expires_at"] || credentials["expiresAt"] || credentials["expiry"]
239
+ return nil unless value
240
+
241
+ case value
242
+ when Time
243
+ value
244
+ when Integer, Float
245
+ Time.at(value)
246
+ when String
247
+ Time.parse(value)
248
+ end
249
+ rescue ArgumentError
250
+ nil
251
+ end
138
252
  end
139
253
  end
140
254
  end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentHarness
4
+ module Providers
5
+ # Mistral Vibe CLI provider
6
+ #
7
+ # Provides integration with the Mistral Vibe CLI agent tool.
8
+ class MistralVibe < Base
9
+ class << self
10
+ def provider_name
11
+ :mistral_vibe
12
+ end
13
+
14
+ def binary_name
15
+ "mistral-vibe"
16
+ end
17
+
18
+ def available?
19
+ executor = AgentHarness.configuration.command_executor
20
+ !!executor.which(binary_name)
21
+ end
22
+
23
+ def firewall_requirements
24
+ {
25
+ domains: [
26
+ "api.mistral.ai"
27
+ ],
28
+ ip_ranges: []
29
+ }
30
+ end
31
+
32
+ def instruction_file_paths
33
+ []
34
+ end
35
+
36
+ def discover_models
37
+ return [] unless available?
38
+ []
39
+ end
40
+ end
41
+
42
+ def name
43
+ "mistral_vibe"
44
+ end
45
+
46
+ def display_name
47
+ "Mistral Vibe CLI"
48
+ end
49
+
50
+ def capabilities
51
+ {
52
+ streaming: false,
53
+ file_upload: false,
54
+ vision: false,
55
+ tool_use: false,
56
+ json_mode: false,
57
+ mcp: false,
58
+ dangerous_mode: false
59
+ }
60
+ end
61
+
62
+ protected
63
+
64
+ def build_command(prompt, options)
65
+ cmd = [self.class.binary_name, "run"]
66
+ cmd << prompt
67
+ cmd
68
+ end
69
+
70
+ def default_timeout
71
+ 300
72
+ end
73
+ end
74
+ end
75
+ end
@@ -123,6 +123,7 @@ module AgentHarness
123
123
  register_if_available(:opencode, "agent_harness/providers/opencode", :Opencode)
124
124
  register_if_available(:kilocode, "agent_harness/providers/kilocode", :Kilocode)
125
125
  register_if_available(:aider, "agent_harness/providers/aider", :Aider)
126
+ register_if_available(:mistral_vibe, "agent_harness/providers/mistral_vibe", :MistralVibe)
126
127
  end
127
128
 
128
129
  def register_if_available(name, require_path, class_name, aliases: [])
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AgentHarness
4
- VERSION = "0.5.2"
4
+ VERSION = "0.5.3"
5
5
  end
data/lib/agent_harness.rb CHANGED
@@ -157,6 +157,7 @@ require_relative "agent_harness/providers/cursor"
157
157
  require_relative "agent_harness/providers/gemini"
158
158
  require_relative "agent_harness/providers/github_copilot"
159
159
  require_relative "agent_harness/providers/kilocode"
160
+ require_relative "agent_harness/providers/mistral_vibe"
160
161
  require_relative "agent_harness/providers/opencode"
161
162
 
162
163
  # Orchestration layer
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: agent-harness
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.2
4
+ version: 0.5.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bart Agapinan
@@ -100,6 +100,7 @@ files:
100
100
  - lib/agent_harness/providers/gemini.rb
101
101
  - lib/agent_harness/providers/github_copilot.rb
102
102
  - lib/agent_harness/providers/kilocode.rb
103
+ - lib/agent_harness/providers/mistral_vibe.rb
103
104
  - lib/agent_harness/providers/opencode.rb
104
105
  - lib/agent_harness/providers/registry.rb
105
106
  - lib/agent_harness/response.rb