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 +4 -4
- data/.release-please-manifest.json +1 -1
- data/AUDIT_DISPOSITION.md +111 -0
- data/CHANGELOG.md +21 -0
- data/README.md +140 -2
- data/lib/agent_harness/authentication.rb +28 -9
- data/lib/agent_harness/command_executor.rb +450 -13
- data/lib/agent_harness/docker_command_executor.rb +499 -8
- data/lib/agent_harness/error_taxonomy.rb +4 -4
- data/lib/agent_harness/execution_preparation.rb +64 -0
- data/lib/agent_harness/orchestration/provider_manager.rb +26 -6
- data/lib/agent_harness/provider_health_check.rb +28 -6
- data/lib/agent_harness/provider_runtime.rb +38 -11
- data/lib/agent_harness/providers/adapter.rb +596 -8
- data/lib/agent_harness/providers/aider.rb +71 -0
- data/lib/agent_harness/providers/anthropic.rb +110 -7
- data/lib/agent_harness/providers/base.rb +34 -5
- data/lib/agent_harness/providers/codex.rb +40 -9
- data/lib/agent_harness/providers/cursor.rb +76 -2
- data/lib/agent_harness/providers/gemini.rb +21 -6
- data/lib/agent_harness/providers/github_copilot.rb +12 -0
- data/lib/agent_harness/providers/kilocode.rb +16 -1
- data/lib/agent_harness/providers/mistral_vibe.rb +9 -0
- data/lib/agent_harness/providers/opencode.rb +64 -3
- data/lib/agent_harness/providers/registry.rb +392 -18
- data/lib/agent_harness/version.rb +1 -1
- data/lib/agent_harness.rb +29 -0
- metadata +3 -1
|
@@ -6,6 +6,17 @@ module AgentHarness
|
|
|
6
6
|
#
|
|
7
7
|
# Provides integration with the Aider CLI tool.
|
|
8
8
|
class Aider < Base
|
|
9
|
+
UV_VERSION = "0.8.17"
|
|
10
|
+
SUPPORTED_CLI_VERSION = "0.86.2"
|
|
11
|
+
SUPPORTED_CLI_REQUIREMENT = Gem::Requirement.new(">= #{SUPPORTED_CLI_VERSION}", "< 0.87.0").freeze
|
|
12
|
+
PYTHON_VERSION = "python3.12"
|
|
13
|
+
BINARY_PATH = "/usr/local/bin/aider"
|
|
14
|
+
UV_TOOL_ENV = {
|
|
15
|
+
"UV_TOOL_BIN_DIR" => "/usr/local/bin",
|
|
16
|
+
"UV_TOOL_DIR" => "/opt/uv/tools",
|
|
17
|
+
"UV_PYTHON_INSTALL_DIR" => "/opt/uv/python"
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
9
20
|
class << self
|
|
10
21
|
def provider_name
|
|
11
22
|
:aider
|
|
@@ -50,6 +61,66 @@ module AgentHarness
|
|
|
50
61
|
]
|
|
51
62
|
end
|
|
52
63
|
|
|
64
|
+
def installation_contract(version: SUPPORTED_CLI_VERSION)
|
|
65
|
+
version = version.strip if version.respond_to?(:strip)
|
|
66
|
+
|
|
67
|
+
unless version.is_a?(String) && !version.empty?
|
|
68
|
+
raise ArgumentError,
|
|
69
|
+
"Unsupported Aider CLI version #{version.inspect}; " \
|
|
70
|
+
"supported versions must satisfy #{SUPPORTED_CLI_REQUIREMENT}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
parsed_version = begin
|
|
74
|
+
Gem::Version.new(version)
|
|
75
|
+
rescue ArgumentError
|
|
76
|
+
raise ArgumentError,
|
|
77
|
+
"Unsupported Aider CLI version #{version.inspect}; " \
|
|
78
|
+
"supported versions must satisfy #{SUPPORTED_CLI_REQUIREMENT}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
unless SUPPORTED_CLI_REQUIREMENT.satisfied_by?(parsed_version)
|
|
82
|
+
raise ArgumentError,
|
|
83
|
+
"Unsupported Aider CLI version #{version.inspect}; " \
|
|
84
|
+
"supported versions must satisfy #{SUPPORTED_CLI_REQUIREMENT}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
default_package = "aider-chat==#{version}".freeze
|
|
88
|
+
bootstrap_command = [
|
|
89
|
+
"python3", "-m", "pip", "install", "--no-cache-dir", "--break-system-packages", "uv==#{UV_VERSION}"
|
|
90
|
+
].freeze
|
|
91
|
+
install_command_prefix = [
|
|
92
|
+
"uv", "tool", "install", "--force", "--python", PYTHON_VERSION, "--with", "pip"
|
|
93
|
+
].freeze
|
|
94
|
+
install_command = (install_command_prefix + [default_package]).freeze
|
|
95
|
+
supported_versions = [version].freeze
|
|
96
|
+
version_requirement = SUPPORTED_CLI_REQUIREMENT.requirements
|
|
97
|
+
.map { |op, ver| "#{op} #{ver}".freeze }
|
|
98
|
+
.freeze
|
|
99
|
+
|
|
100
|
+
contract = {
|
|
101
|
+
source: :uv_tool,
|
|
102
|
+
bootstrap_source: :pip,
|
|
103
|
+
bootstrap_package: "uv==#{UV_VERSION}",
|
|
104
|
+
bootstrap_commands: [bootstrap_command].freeze,
|
|
105
|
+
install_environment: UV_TOOL_ENV,
|
|
106
|
+
package: default_package,
|
|
107
|
+
package_name: "aider-chat",
|
|
108
|
+
version: version,
|
|
109
|
+
version_format: "%{package_name}==%{version}",
|
|
110
|
+
version_requirement: version_requirement,
|
|
111
|
+
binary_name: binary_name,
|
|
112
|
+
binary_path: BINARY_PATH,
|
|
113
|
+
install_command_prefix: install_command_prefix,
|
|
114
|
+
install_command: install_command,
|
|
115
|
+
supported_versions: supported_versions
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
contract.each_value do |value|
|
|
119
|
+
value.freeze if value.is_a?(String)
|
|
120
|
+
end
|
|
121
|
+
contract.freeze
|
|
122
|
+
end
|
|
123
|
+
|
|
53
124
|
def smoke_test_contract
|
|
54
125
|
Base::DEFAULT_SMOKE_TEST_CONTRACT
|
|
55
126
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require "shellwords"
|
|
4
5
|
|
|
5
6
|
module AgentHarness
|
|
6
7
|
module Providers
|
|
@@ -15,6 +16,12 @@ module AgentHarness
|
|
|
15
16
|
class Anthropic < Base
|
|
16
17
|
# Model name pattern for Anthropic Claude models
|
|
17
18
|
MODEL_PATTERN = /^claude-[\d.-]+-(?:opus|sonnet|haiku)(?:-\d{8})?$/i
|
|
19
|
+
SUPPORTED_CLI_VERSION = "2.1.92"
|
|
20
|
+
SUPPORTED_CLI_REQUIREMENT = Gem::Requirement.new(">= #{SUPPORTED_CLI_VERSION}", "< 2.2.0").freeze
|
|
21
|
+
|
|
22
|
+
# Matches semver (e.g. "2.1.92"), optional pre-release (e.g. "2.1.92-beta.1"),
|
|
23
|
+
# or channel tokens (e.g. "latest", "stable").
|
|
24
|
+
VALID_VERSION_PATTERN = /\A(?:\d+\.\d+\.\d+(?:-[a-zA-Z0-9.]+)?|latest|stable)\z/
|
|
18
25
|
|
|
19
26
|
class << self
|
|
20
27
|
def provider_name
|
|
@@ -25,11 +32,80 @@ module AgentHarness
|
|
|
25
32
|
"claude"
|
|
26
33
|
end
|
|
27
34
|
|
|
35
|
+
def install_contract(version: nil)
|
|
36
|
+
target_version = version.nil? ? SUPPORTED_CLI_VERSION : version
|
|
37
|
+
target_version = target_version.strip if target_version.respond_to?(:strip)
|
|
38
|
+
validate_version!(target_version)
|
|
39
|
+
version_requirement = SUPPORTED_CLI_REQUIREMENT.requirements
|
|
40
|
+
.map { |op, ver| "#{op} #{ver}" }
|
|
41
|
+
.join(", ")
|
|
42
|
+
channel_token = %w[latest stable].include?(target_version.to_s)
|
|
43
|
+
|
|
44
|
+
warning = "Review the downloaded installer before execution and verify any published checksum or signature metadata when available."
|
|
45
|
+
if channel_token
|
|
46
|
+
warning += " Channel '#{target_version}' is not pinned; the resolved version may fall " \
|
|
47
|
+
"outside the supported range (#{version_requirement}). Verify the installed version " \
|
|
48
|
+
"after installation."
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
{
|
|
52
|
+
provider: provider_name,
|
|
53
|
+
binary_name: binary_name,
|
|
54
|
+
binary_paths: [
|
|
55
|
+
"$HOME/.local/bin/claude",
|
|
56
|
+
binary_name
|
|
57
|
+
],
|
|
58
|
+
install: {
|
|
59
|
+
strategy: :shell,
|
|
60
|
+
source: "official",
|
|
61
|
+
command: "tmp_script=$(mktemp) && trap 'rm -f \"$tmp_script\"' EXIT && curl -fsSL https://claude.ai/install.sh -o \"$tmp_script\" && bash \"$tmp_script\" #{Shellwords.shellescape(target_version)}",
|
|
62
|
+
warning: warning,
|
|
63
|
+
post_install_binary_path: "$HOME/.local/bin/claude",
|
|
64
|
+
# When a channel token is used, include the requirement so
|
|
65
|
+
# consumers can validate the installed version post-install.
|
|
66
|
+
version_not_pinned: channel_token
|
|
67
|
+
},
|
|
68
|
+
supported_versions: {
|
|
69
|
+
default: SUPPORTED_CLI_VERSION,
|
|
70
|
+
requirement: version_requirement,
|
|
71
|
+
channel: "stable"
|
|
72
|
+
},
|
|
73
|
+
runtime_contract: {
|
|
74
|
+
available_via: binary_name,
|
|
75
|
+
build_command: [
|
|
76
|
+
binary_name,
|
|
77
|
+
"--print",
|
|
78
|
+
"--output-format=json"
|
|
79
|
+
],
|
|
80
|
+
required_features: [
|
|
81
|
+
"print_mode",
|
|
82
|
+
"json_output",
|
|
83
|
+
"mcp_config",
|
|
84
|
+
"mcp_list",
|
|
85
|
+
"dangerously_skip_permissions",
|
|
86
|
+
"models_list"
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
|
|
28
92
|
def available?
|
|
29
93
|
executor = AgentHarness.configuration.command_executor
|
|
30
94
|
!!executor.which(binary_name)
|
|
31
95
|
end
|
|
32
96
|
|
|
97
|
+
def provider_metadata_overrides
|
|
98
|
+
{
|
|
99
|
+
auth: {
|
|
100
|
+
service: :anthropic,
|
|
101
|
+
api_family: :anthropic
|
|
102
|
+
},
|
|
103
|
+
identity: {
|
|
104
|
+
bot_usernames: %w[claude anthropic]
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
|
|
33
109
|
def firewall_requirements
|
|
34
110
|
{
|
|
35
111
|
domains: [
|
|
@@ -87,6 +163,33 @@ module AgentHarness
|
|
|
87
163
|
|
|
88
164
|
private
|
|
89
165
|
|
|
166
|
+
def validate_version!(version)
|
|
167
|
+
unless version.is_a?(String) && !version.strip.empty?
|
|
168
|
+
raise ArgumentError, "Invalid version: #{version.inspect}. " \
|
|
169
|
+
"Must be a semver string (e.g. '2.1.92'), optional pre-release suffix, or a channel token ('latest', 'stable')."
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
version_str = version.strip
|
|
173
|
+
|
|
174
|
+
unless VALID_VERSION_PATTERN.match?(version_str)
|
|
175
|
+
raise ArgumentError, "Invalid version: #{version.inspect}. " \
|
|
176
|
+
"Must be a semver string (e.g. '2.1.92'), optional pre-release suffix, or a channel token ('latest', 'stable')."
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
return if %w[latest stable].include?(version_str)
|
|
180
|
+
|
|
181
|
+
gem_version = begin
|
|
182
|
+
Gem::Version.new(version_str)
|
|
183
|
+
rescue ArgumentError
|
|
184
|
+
raise ArgumentError, "Invalid version: #{version.inspect}. " \
|
|
185
|
+
"Must be a semver string (e.g. '2.1.92'), optional pre-release suffix, or a channel token ('latest', 'stable')."
|
|
186
|
+
end
|
|
187
|
+
return if SUPPORTED_CLI_REQUIREMENT.satisfied_by?(gem_version)
|
|
188
|
+
|
|
189
|
+
raise ArgumentError, "Version #{version.inspect} is outside the supported range " \
|
|
190
|
+
"(#{SUPPORTED_CLI_REQUIREMENT}). Update SUPPORTED_CLI_REQUIREMENT before targeting this version."
|
|
191
|
+
end
|
|
192
|
+
|
|
90
193
|
def parse_models_list(output)
|
|
91
194
|
return [] if output.nil? || output.empty?
|
|
92
195
|
|
|
@@ -240,7 +343,7 @@ module AgentHarness
|
|
|
240
343
|
rate_limited: [
|
|
241
344
|
/rate.?limit/i,
|
|
242
345
|
/too.?many.?requests/i,
|
|
243
|
-
|
|
346
|
+
/\b429\b/,
|
|
244
347
|
/overloaded/i,
|
|
245
348
|
/session.?limit/i
|
|
246
349
|
],
|
|
@@ -249,7 +352,7 @@ module AgentHarness
|
|
|
249
352
|
/authentication.*error/i,
|
|
250
353
|
/invalid.*api.*key/i,
|
|
251
354
|
/unauthorized/i,
|
|
252
|
-
|
|
355
|
+
/\b401\b/,
|
|
253
356
|
/session.*expired/i,
|
|
254
357
|
/not.*logged.*in/i,
|
|
255
358
|
/login.*required/i,
|
|
@@ -265,17 +368,17 @@ module AgentHarness
|
|
|
265
368
|
/connection.*reset/i,
|
|
266
369
|
/temporary.*error/i,
|
|
267
370
|
/service.*unavailable/i,
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
/
|
|
371
|
+
/\b503\b/,
|
|
372
|
+
/\b502\b/,
|
|
373
|
+
/\b504\b/
|
|
271
374
|
],
|
|
272
375
|
permanent: [
|
|
273
376
|
/invalid.*model/i,
|
|
274
377
|
/unsupported.*operation/i,
|
|
275
378
|
/not.*found/i,
|
|
276
|
-
|
|
379
|
+
/\b404\b/,
|
|
277
380
|
/bad.*request/i,
|
|
278
|
-
|
|
381
|
+
/\b400\b/,
|
|
279
382
|
/model.*deprecated/i,
|
|
280
383
|
/end-of-life/i
|
|
281
384
|
]
|
|
@@ -41,7 +41,7 @@ module AgentHarness
|
|
|
41
41
|
rate_limited: [
|
|
42
42
|
/rate.?limit/i,
|
|
43
43
|
/too.?many.?requests/i,
|
|
44
|
-
/
|
|
44
|
+
/\b429\b/
|
|
45
45
|
],
|
|
46
46
|
auth_expired: [
|
|
47
47
|
/invalid.*api.*key/i,
|
|
@@ -57,8 +57,8 @@ module AgentHarness
|
|
|
57
57
|
/timeout/i,
|
|
58
58
|
/connection.*error/i,
|
|
59
59
|
/service.*unavailable/i,
|
|
60
|
-
|
|
61
|
-
/
|
|
60
|
+
/\b503\b/,
|
|
61
|
+
/\b502\b/
|
|
62
62
|
]
|
|
63
63
|
}.tap { |patterns| patterns.each_value(&:freeze) }.freeze
|
|
64
64
|
|
|
@@ -98,6 +98,8 @@ module AgentHarness
|
|
|
98
98
|
# @option options [ProviderRuntime, Hash, nil] :provider_runtime per-request
|
|
99
99
|
# runtime overrides (model, base_url, api_provider, env, flags, metadata).
|
|
100
100
|
# A plain Hash is automatically coerced into a ProviderRuntime.
|
|
101
|
+
# Providers can derive request-scoped execution preparation from this
|
|
102
|
+
# runtime to materialize config files or other bootstrap state.
|
|
101
103
|
# @return [Response] the response
|
|
102
104
|
def send_message(prompt:, **options)
|
|
103
105
|
log_debug("send_message_start", prompt_length: prompt.length, options: options.keys)
|
|
@@ -111,6 +113,7 @@ module AgentHarness
|
|
|
111
113
|
|
|
112
114
|
# Build command
|
|
113
115
|
command = build_command(prompt, options)
|
|
116
|
+
preparation = build_execution_preparation(options)
|
|
114
117
|
|
|
115
118
|
# Calculate timeout
|
|
116
119
|
timeout = options[:timeout] || @config.timeout || default_timeout
|
|
@@ -121,6 +124,7 @@ module AgentHarness
|
|
|
121
124
|
command,
|
|
122
125
|
timeout: timeout,
|
|
123
126
|
env: build_env(options),
|
|
127
|
+
preparation: preparation,
|
|
124
128
|
**command_execution_options(options)
|
|
125
129
|
)
|
|
126
130
|
duration = Time.now - start_time
|
|
@@ -210,6 +214,17 @@ module AgentHarness
|
|
|
210
214
|
env
|
|
211
215
|
end
|
|
212
216
|
|
|
217
|
+
# Build structured runtime preparation for the executor.
|
|
218
|
+
#
|
|
219
|
+
# Provider subclasses can override to request request-scoped file writes
|
|
220
|
+
# or other bootstrap work without shell-wrapping the main command.
|
|
221
|
+
#
|
|
222
|
+
# @param options [Hash] options
|
|
223
|
+
# @return [ExecutionPreparation, nil] preparation contract or nil
|
|
224
|
+
def build_execution_preparation(options)
|
|
225
|
+
nil
|
|
226
|
+
end
|
|
227
|
+
|
|
213
228
|
# Parse CLI output into Response - override in subclasses
|
|
214
229
|
#
|
|
215
230
|
# Combines stdout and stderr for error classification so that
|
|
@@ -309,8 +324,22 @@ module AgentHarness
|
|
|
309
324
|
execution_options
|
|
310
325
|
end
|
|
311
326
|
|
|
312
|
-
def execute_with_timeout(command, timeout:, env:, stdin_data: nil, **execution_options)
|
|
313
|
-
|
|
327
|
+
def execute_with_timeout(command, timeout:, env:, preparation: nil, stdin_data: nil, **execution_options)
|
|
328
|
+
kwargs = {timeout: timeout, env: env}
|
|
329
|
+
kwargs[:stdin_data] = stdin_data unless stdin_data.nil?
|
|
330
|
+
kwargs[:preparation] = preparation unless preparation.nil?
|
|
331
|
+
kwargs.merge!(execution_options)
|
|
332
|
+
|
|
333
|
+
@executor.execute(command, **kwargs)
|
|
334
|
+
rescue ArgumentError => e
|
|
335
|
+
unknown_keyword_message = e.message
|
|
336
|
+
raise unless unknown_keyword_message.start_with?("unknown keyword", "unknown keywords")
|
|
337
|
+
raise unless unknown_keyword_message.include?(":preparation")
|
|
338
|
+
|
|
339
|
+
raise ProviderError.new(
|
|
340
|
+
"Injected executor #{@executor.class}#execute must accept the preparation: keyword argument",
|
|
341
|
+
original_error: e
|
|
342
|
+
)
|
|
314
343
|
end
|
|
315
344
|
|
|
316
345
|
def track_tokens(response)
|
|
@@ -25,6 +25,15 @@ module AgentHarness
|
|
|
25
25
|
!!executor.which(binary_name)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
def provider_metadata_overrides
|
|
29
|
+
{
|
|
30
|
+
auth: {
|
|
31
|
+
service: :openai,
|
|
32
|
+
api_family: :openai
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
28
37
|
def firewall_requirements
|
|
29
38
|
{
|
|
30
39
|
domains: [
|
|
@@ -53,11 +62,33 @@ module AgentHarness
|
|
|
53
62
|
]
|
|
54
63
|
end
|
|
55
64
|
|
|
56
|
-
def installation_contract
|
|
57
|
-
|
|
65
|
+
def installation_contract(version: SUPPORTED_CLI_VERSION)
|
|
66
|
+
version = version.strip if version.respond_to?(:strip)
|
|
67
|
+
|
|
68
|
+
unless version.is_a?(String) && !version.empty?
|
|
69
|
+
raise ArgumentError,
|
|
70
|
+
"Unsupported Codex CLI version #{version.inspect}; " \
|
|
71
|
+
"supported versions must satisfy #{SUPPORTED_CLI_REQUIREMENT}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
parsed_version = begin
|
|
75
|
+
Gem::Version.new(version)
|
|
76
|
+
rescue ArgumentError
|
|
77
|
+
raise ArgumentError,
|
|
78
|
+
"Unsupported Codex CLI version #{version.inspect}; " \
|
|
79
|
+
"supported versions must satisfy #{SUPPORTED_CLI_REQUIREMENT}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
unless SUPPORTED_CLI_REQUIREMENT.satisfied_by?(parsed_version)
|
|
83
|
+
raise ArgumentError,
|
|
84
|
+
"Unsupported Codex CLI version #{version.inspect}; " \
|
|
85
|
+
"supported versions must satisfy #{SUPPORTED_CLI_REQUIREMENT}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
default_package = "@openai/codex@#{version}".freeze
|
|
58
89
|
install_command_prefix = ["npm", "install", "-g", "--ignore-scripts"].freeze
|
|
59
90
|
install_command = (install_command_prefix + [default_package]).freeze
|
|
60
|
-
supported_versions = [
|
|
91
|
+
supported_versions = [version].freeze
|
|
61
92
|
version_requirement = SUPPORTED_CLI_REQUIREMENT.requirements
|
|
62
93
|
.map { |op, ver| "#{op} #{ver}".freeze }
|
|
63
94
|
.freeze
|
|
@@ -66,7 +97,7 @@ module AgentHarness
|
|
|
66
97
|
source: :npm,
|
|
67
98
|
package: default_package,
|
|
68
99
|
package_name: "@openai/codex",
|
|
69
|
-
version:
|
|
100
|
+
version: version,
|
|
70
101
|
version_requirement: version_requirement,
|
|
71
102
|
binary_name: binary_name,
|
|
72
103
|
install_command_prefix: install_command_prefix,
|
|
@@ -141,7 +172,7 @@ module AgentHarness
|
|
|
141
172
|
|
|
142
173
|
def error_patterns
|
|
143
174
|
COMMON_ERROR_PATTERNS.merge(
|
|
144
|
-
auth_expired: COMMON_ERROR_PATTERNS[:auth_expired] + [
|
|
175
|
+
auth_expired: COMMON_ERROR_PATTERNS[:auth_expired] + [/\b401\b/, /incorrect.*api.*key/i],
|
|
145
176
|
transient: COMMON_ERROR_PATTERNS[:transient] + [/connection.*reset/i],
|
|
146
177
|
sandbox_failure: [
|
|
147
178
|
/bwrap.*no permissions/i,
|
|
@@ -157,7 +188,7 @@ module AgentHarness
|
|
|
157
188
|
if api_key.strip.start_with?("sk-")
|
|
158
189
|
return {valid: true, expires_at: nil, error: nil, auth_method: :api_key}
|
|
159
190
|
else
|
|
160
|
-
return {valid: false, expires_at: nil, error: "OPENAI_API_KEY is set but does not appear to be a valid OpenAI API key"}
|
|
191
|
+
return {valid: false, expires_at: nil, error: "OPENAI_API_KEY is set but does not appear to be a valid OpenAI API key", auth_method: nil}
|
|
161
192
|
end
|
|
162
193
|
end
|
|
163
194
|
|
|
@@ -168,14 +199,14 @@ module AgentHarness
|
|
|
168
199
|
if key.strip.start_with?("sk-")
|
|
169
200
|
return {valid: true, expires_at: nil, error: nil, auth_method: :config_file}
|
|
170
201
|
else
|
|
171
|
-
return {valid: false, expires_at: nil, error: "Config file API key is set but does not appear to be a valid OpenAI API key"}
|
|
202
|
+
return {valid: false, expires_at: nil, error: "Config file API key is set but does not appear to be a valid OpenAI API key", auth_method: nil}
|
|
172
203
|
end
|
|
173
204
|
end
|
|
174
205
|
end
|
|
175
206
|
|
|
176
|
-
{valid: false, expires_at: nil, error: "No OpenAI API key found. Set OPENAI_API_KEY or configure in #{codex_config_path}"}
|
|
207
|
+
{valid: false, expires_at: nil, error: "No OpenAI API key found. Set OPENAI_API_KEY or configure in #{codex_config_path}", auth_method: nil}
|
|
177
208
|
rescue IOError, JSON::ParserError => e
|
|
178
|
-
{valid: false, expires_at: nil, error: e.message}
|
|
209
|
+
{valid: false, expires_at: nil, error: e.message, auth_method: nil}
|
|
179
210
|
end
|
|
180
211
|
|
|
181
212
|
def health_status
|
|
@@ -12,6 +12,12 @@ module AgentHarness
|
|
|
12
12
|
# provider = AgentHarness::Providers::Cursor.new
|
|
13
13
|
# response = provider.send_message(prompt: "Hello!")
|
|
14
14
|
class Cursor < Base
|
|
15
|
+
INSTALL_SCRIPT_URL = "https://cursor.com/install"
|
|
16
|
+
INSTALL_TARGET_LATEST = "latest"
|
|
17
|
+
INSTALL_BUILD = "2026.03.30-a5d3e17"
|
|
18
|
+
INSTALL_SCRIPT_SHA256 = "8371988b483abec13c07c10e95cccc839da81ebf9596e430d3c90835a227cbad"
|
|
19
|
+
INSTALL_LINUX_X64_PACKAGE_SHA256 = "e0d4b611db111d2dbe76474386271bff3e1dbb2cc6ddf527f9d5d5801b2ce2a0"
|
|
20
|
+
|
|
15
21
|
class << self
|
|
16
22
|
def provider_name
|
|
17
23
|
:cursor
|
|
@@ -26,6 +32,15 @@ module AgentHarness
|
|
|
26
32
|
!!executor.which(binary_name)
|
|
27
33
|
end
|
|
28
34
|
|
|
35
|
+
def provider_metadata_overrides
|
|
36
|
+
{
|
|
37
|
+
auth: {
|
|
38
|
+
service: :cursor,
|
|
39
|
+
api_family: :cursor
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
|
|
29
44
|
def firewall_requirements
|
|
30
45
|
{
|
|
31
46
|
domains: [
|
|
@@ -84,9 +99,66 @@ module AgentHarness
|
|
|
84
99
|
family_name.match?(/^(claude|gpt|cursor)-/)
|
|
85
100
|
end
|
|
86
101
|
|
|
102
|
+
def install_metadata(version: nil)
|
|
103
|
+
install_target = normalize_install_target(version)
|
|
104
|
+
linux_x64_package_url = package_url_for(os: "linux", arch: "x64")
|
|
105
|
+
|
|
106
|
+
{
|
|
107
|
+
source: {
|
|
108
|
+
type: :shell_script,
|
|
109
|
+
url: INSTALL_SCRIPT_URL,
|
|
110
|
+
resolved_version: INSTALL_BUILD,
|
|
111
|
+
default_artifact_url: linux_x64_package_url
|
|
112
|
+
},
|
|
113
|
+
checksum: {
|
|
114
|
+
strategy: :sha256,
|
|
115
|
+
targets: {
|
|
116
|
+
script: {
|
|
117
|
+
url: INSTALL_SCRIPT_URL,
|
|
118
|
+
value: INSTALL_SCRIPT_SHA256
|
|
119
|
+
},
|
|
120
|
+
artifacts: {
|
|
121
|
+
"linux/x64" => {
|
|
122
|
+
url: linux_x64_package_url,
|
|
123
|
+
value: INSTALL_LINUX_X64_PACKAGE_SHA256
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
binary: {
|
|
129
|
+
name: binary_name,
|
|
130
|
+
path: "$HOME/.local/bin/#{binary_name}",
|
|
131
|
+
suggested_global_path: "/usr/local/bin/#{binary_name}"
|
|
132
|
+
},
|
|
133
|
+
version: {
|
|
134
|
+
default: INSTALL_TARGET_LATEST,
|
|
135
|
+
supported: install_target,
|
|
136
|
+
command: [binary_name, "--version"]
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
end
|
|
140
|
+
|
|
87
141
|
def smoke_test_contract
|
|
88
142
|
Base::DEFAULT_SMOKE_TEST_CONTRACT
|
|
89
143
|
end
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
def package_url_for(os:, arch:)
|
|
148
|
+
format(
|
|
149
|
+
"https://downloads.cursor.com/lab/%<build>s/%<os>s/%<arch>s/agent-cli-package.tar.gz",
|
|
150
|
+
build: INSTALL_BUILD,
|
|
151
|
+
os: os,
|
|
152
|
+
arch: arch
|
|
153
|
+
)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def normalize_install_target(version)
|
|
157
|
+
target = version.nil? ? INSTALL_TARGET_LATEST : version.to_s
|
|
158
|
+
return target if target == INSTALL_TARGET_LATEST
|
|
159
|
+
|
|
160
|
+
raise ArgumentError, "Unsupported Cursor install target: #{version.inspect}"
|
|
161
|
+
end
|
|
90
162
|
end
|
|
91
163
|
|
|
92
164
|
def name
|
|
@@ -156,7 +228,7 @@ module AgentHarness
|
|
|
156
228
|
rate_limited: [
|
|
157
229
|
/rate.?limit/i,
|
|
158
230
|
/too.?many.?requests/i,
|
|
159
|
-
/
|
|
231
|
+
/\b429\b/
|
|
160
232
|
],
|
|
161
233
|
auth_expired: [
|
|
162
234
|
/authentication.*error/i,
|
|
@@ -192,12 +264,14 @@ module AgentHarness
|
|
|
192
264
|
|
|
193
265
|
# Execute command with prompt on stdin
|
|
194
266
|
env = build_env(options)
|
|
267
|
+
preparation = build_execution_preparation(options)
|
|
195
268
|
start_time = Time.now
|
|
196
269
|
result = execute_with_timeout(
|
|
197
270
|
command,
|
|
198
271
|
timeout: timeout,
|
|
199
272
|
env: env,
|
|
200
273
|
stdin_data: prompt,
|
|
274
|
+
preparation: preparation,
|
|
201
275
|
**command_execution_options(options)
|
|
202
276
|
)
|
|
203
277
|
duration = Time.now - start_time
|
|
@@ -253,7 +327,7 @@ module AgentHarness
|
|
|
253
327
|
return nil unless self.class.available?
|
|
254
328
|
|
|
255
329
|
begin
|
|
256
|
-
result = @executor.execute([
|
|
330
|
+
result = @executor.execute([self.class.binary_name, "mcp", "list"], timeout: 5)
|
|
257
331
|
return nil unless result.success?
|
|
258
332
|
|
|
259
333
|
parse_mcp_servers_output(result.stdout)
|
|
@@ -30,7 +30,22 @@ module AgentHarness
|
|
|
30
30
|
!!executor.which(binary_name)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
def provider_metadata_overrides
|
|
34
|
+
{
|
|
35
|
+
auth: {
|
|
36
|
+
service: :google,
|
|
37
|
+
api_family: :gemini
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
|
|
33
42
|
def install_contract(version: SUPPORTED_CLI_VERSION)
|
|
43
|
+
version = version.strip if version.respond_to?(:strip)
|
|
44
|
+
|
|
45
|
+
unless version.is_a?(String) && !version.empty?
|
|
46
|
+
raise ArgumentError, "Unsupported Gemini CLI version #{version.inspect}. Supported requirement: #{SUPPORTED_CLI_REQUIREMENT}"
|
|
47
|
+
end
|
|
48
|
+
|
|
34
49
|
parsed_version = begin
|
|
35
50
|
Gem::Version.new(version)
|
|
36
51
|
rescue ArgumentError
|
|
@@ -170,7 +185,7 @@ module AgentHarness
|
|
|
170
185
|
rate_limited: [
|
|
171
186
|
/rate.?limit/i,
|
|
172
187
|
/quota.?exceeded/i,
|
|
173
|
-
/
|
|
188
|
+
/\b429\b/
|
|
174
189
|
],
|
|
175
190
|
auth_expired: [
|
|
176
191
|
/authentication/i,
|
|
@@ -184,7 +199,7 @@ module AgentHarness
|
|
|
184
199
|
transient: [
|
|
185
200
|
/timeout/i,
|
|
186
201
|
/temporary/i,
|
|
187
|
-
/
|
|
202
|
+
/\b503\b/
|
|
188
203
|
]
|
|
189
204
|
}
|
|
190
205
|
end
|
|
@@ -196,21 +211,21 @@ module AgentHarness
|
|
|
196
211
|
end
|
|
197
212
|
|
|
198
213
|
credentials = read_gemini_credentials
|
|
199
|
-
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
|
|
214
|
+
return {valid: false, expires_at: nil, error: "No Gemini credentials found. Run 'gemini auth login' or set GEMINI_API_KEY or GOOGLE_API_KEY", auth_method: nil} unless credentials
|
|
200
215
|
|
|
201
216
|
token = credentials["access_token"] || credentials["oauth_token"]
|
|
202
217
|
unless token.is_a?(String) && !token.strip.empty?
|
|
203
|
-
return {valid: false, expires_at: nil, error: "No authentication token in Gemini credentials"}
|
|
218
|
+
return {valid: false, expires_at: nil, error: "No authentication token in Gemini credentials", auth_method: nil}
|
|
204
219
|
end
|
|
205
220
|
|
|
206
221
|
expires_at = parse_gemini_expiry(credentials)
|
|
207
222
|
if expires_at && expires_at < Time.now
|
|
208
|
-
{valid: false, expires_at: expires_at, error: "Gemini session expired. Run 'gemini auth login' to re-authenticate"}
|
|
223
|
+
{valid: false, expires_at: expires_at, error: "Gemini session expired. Run 'gemini auth login' to re-authenticate", auth_method: :oauth}
|
|
209
224
|
else
|
|
210
225
|
{valid: true, expires_at: expires_at, error: nil, auth_method: :oauth}
|
|
211
226
|
end
|
|
212
227
|
rescue IOError, JSON::ParserError => e
|
|
213
|
-
{valid: false, expires_at: nil, error: e.message}
|
|
228
|
+
{valid: false, expires_at: nil, error: e.message, auth_method: nil}
|
|
214
229
|
end
|
|
215
230
|
|
|
216
231
|
def health_status
|
|
@@ -37,6 +37,18 @@ module AgentHarness
|
|
|
37
37
|
!!executor.which(binary_name)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
def provider_metadata_overrides
|
|
41
|
+
{
|
|
42
|
+
auth: {
|
|
43
|
+
service: :github,
|
|
44
|
+
api_family: :github_copilot
|
|
45
|
+
},
|
|
46
|
+
identity: {
|
|
47
|
+
bot_usernames: ["github-copilot[bot]"]
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
|
|
40
52
|
def firewall_requirements
|
|
41
53
|
{
|
|
42
54
|
domains: [
|
|
@@ -41,6 +41,7 @@ module AgentHarness
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def installation_contract(version: DEFAULT_VERSION)
|
|
44
|
+
version = version.strip if version.respond_to?(:strip)
|
|
44
45
|
validate_install_version!(version)
|
|
45
46
|
package_spec = "#{PACKAGE_NAME}@#{version}"
|
|
46
47
|
|
|
@@ -67,8 +68,22 @@ module AgentHarness
|
|
|
67
68
|
private
|
|
68
69
|
|
|
69
70
|
def validate_install_version!(version)
|
|
71
|
+
unless version.is_a?(String) && !version.strip.empty?
|
|
72
|
+
raise ArgumentError,
|
|
73
|
+
"Unsupported Kilocode CLI version #{version.inspect}; " \
|
|
74
|
+
"supported versions must satisfy #{SUPPORTED_VERSION_REQUIREMENT}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
parsed_version = begin
|
|
78
|
+
Gem::Version.new(version)
|
|
79
|
+
rescue ArgumentError
|
|
80
|
+
raise ArgumentError,
|
|
81
|
+
"Unsupported Kilocode CLI version #{version.inspect}; " \
|
|
82
|
+
"supported versions must satisfy #{SUPPORTED_VERSION_REQUIREMENT}"
|
|
83
|
+
end
|
|
84
|
+
|
|
70
85
|
requirement = Gem::Requirement.new(SUPPORTED_VERSION_REQUIREMENT)
|
|
71
|
-
return if requirement.satisfied_by?(
|
|
86
|
+
return if requirement.satisfied_by?(parsed_version)
|
|
72
87
|
|
|
73
88
|
raise ArgumentError,
|
|
74
89
|
"Unsupported Kilocode CLI version #{version.inspect}; " \
|