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 +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +7 -0
- data/lib/agent_harness/providers/codex.rb +118 -0
- data/lib/agent_harness/providers/gemini.rb +116 -2
- data/lib/agent_harness/providers/mistral_vibe.rb +75 -0
- data/lib/agent_harness/providers/registry.rb +1 -0
- data/lib/agent_harness/version.rb +1 -1
- data/lib/agent_harness.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3dfde368468f5c0c037ec1cb2fefd705e813d192e3723fd70126757cd851cf14
|
|
4
|
+
data.tar.gz: f68f11de99ea61807ab1bb7a94a28aca4aa37e3346469b195a77019f6ceeceec
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4cf0d1807fee47eb2ef1aecc63ab8c10ce71a681d6c98ca2cb860ae7eb9ba7b1a8d88e4382e9ff1f5f2f25acd9380a6cf4c3d8e61f5cf008d920b4d2bbf7b4bc
|
|
7
|
+
data.tar.gz: cd6ac3ae08acf302369a28cdda0a3e332994f679ad37258c0f4dd869f99f18396ccb1cb52d9a6d5bcbb3c6ac3c776ba87a5338edb8280b46785edce254eb8258
|
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
|
-
|
|
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: [])
|
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.
|
|
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
|