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.
@@ -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,50 @@ module AgentHarness
50
61
  ]
51
62
  end
52
63
 
64
+ def installation_contract(version: SUPPORTED_CLI_VERSION)
65
+ unless SUPPORTED_CLI_REQUIREMENT.satisfied_by?(Gem::Version.new(version))
66
+ raise ArgumentError,
67
+ "Unsupported Aider CLI version #{version.inspect}; " \
68
+ "supported versions must satisfy #{SUPPORTED_CLI_REQUIREMENT}"
69
+ end
70
+
71
+ default_package = "aider-chat==#{version}".freeze
72
+ bootstrap_command = [
73
+ "python3", "-m", "pip", "install", "--no-cache-dir", "--break-system-packages", "uv==#{UV_VERSION}"
74
+ ].freeze
75
+ install_command_prefix = [
76
+ "uv", "tool", "install", "--force", "--python", PYTHON_VERSION, "--with", "pip"
77
+ ].freeze
78
+ install_command = (install_command_prefix + [default_package]).freeze
79
+ supported_versions = [version].freeze
80
+ version_requirement = SUPPORTED_CLI_REQUIREMENT.requirements
81
+ .map { |op, ver| "#{op} #{ver}".freeze }
82
+ .freeze
83
+
84
+ contract = {
85
+ source: :uv_tool,
86
+ bootstrap_source: :pip,
87
+ bootstrap_package: "uv==#{UV_VERSION}",
88
+ bootstrap_commands: [bootstrap_command].freeze,
89
+ install_environment: UV_TOOL_ENV,
90
+ package: default_package,
91
+ package_name: "aider-chat",
92
+ version: version,
93
+ version_format: "%{package_name}==%{version}",
94
+ version_requirement: version_requirement,
95
+ binary_name: binary_name,
96
+ binary_path: BINARY_PATH,
97
+ install_command_prefix: install_command_prefix,
98
+ install_command: install_command,
99
+ supported_versions: supported_versions
100
+ }
101
+
102
+ contract.each_value do |value|
103
+ value.freeze if value.is_a?(String)
104
+ end
105
+ contract.freeze
106
+ end
107
+
53
108
  def smoke_test_contract
54
109
  Base::DEFAULT_SMOKE_TEST_CONTRACT
55
110
  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,79 @@ module AgentHarness
25
32
  "claude"
26
33
  end
27
34
 
35
+ def install_contract(version: nil)
36
+ target_version = version || SUPPORTED_CLI_VERSION
37
+ validate_version!(target_version)
38
+ version_requirement = SUPPORTED_CLI_REQUIREMENT.requirements
39
+ .map { |op, ver| "#{op} #{ver}" }
40
+ .join(", ")
41
+ channel_token = %w[latest stable].include?(target_version.to_s)
42
+
43
+ warning = "Review the downloaded installer before execution and verify any published checksum or signature metadata when available."
44
+ if channel_token
45
+ warning += " Channel '#{target_version}' is not pinned; the resolved version may fall " \
46
+ "outside the supported range (#{version_requirement}). Verify the installed version " \
47
+ "after installation."
48
+ end
49
+
50
+ {
51
+ provider: provider_name,
52
+ binary_name: binary_name,
53
+ binary_paths: [
54
+ "$HOME/.local/bin/claude",
55
+ binary_name
56
+ ],
57
+ install: {
58
+ strategy: :shell,
59
+ source: "official",
60
+ 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)}",
61
+ warning: warning,
62
+ post_install_binary_path: "$HOME/.local/bin/claude",
63
+ # When a channel token is used, include the requirement so
64
+ # consumers can validate the installed version post-install.
65
+ version_not_pinned: channel_token
66
+ },
67
+ supported_versions: {
68
+ default: SUPPORTED_CLI_VERSION,
69
+ requirement: version_requirement,
70
+ channel: "stable"
71
+ },
72
+ runtime_contract: {
73
+ available_via: binary_name,
74
+ build_command: [
75
+ binary_name,
76
+ "--print",
77
+ "--output-format=json"
78
+ ],
79
+ required_features: [
80
+ "print_mode",
81
+ "json_output",
82
+ "mcp_config",
83
+ "mcp_list",
84
+ "dangerously_skip_permissions",
85
+ "models_list"
86
+ ]
87
+ }
88
+ }
89
+ end
90
+
28
91
  def available?
29
92
  executor = AgentHarness.configuration.command_executor
30
93
  !!executor.which(binary_name)
31
94
  end
32
95
 
96
+ def provider_metadata_overrides
97
+ {
98
+ auth: {
99
+ service: :anthropic,
100
+ api_family: :anthropic
101
+ },
102
+ identity: {
103
+ bot_usernames: %w[claude anthropic]
104
+ }
105
+ }
106
+ end
107
+
33
108
  def firewall_requirements
34
109
  {
35
110
  domains: [
@@ -87,6 +162,25 @@ module AgentHarness
87
162
 
88
163
  private
89
164
 
165
+ def validate_version!(version)
166
+ version_str = version.to_s
167
+
168
+ unless VALID_VERSION_PATTERN.match?(version_str)
169
+ raise ArgumentError, "Invalid version: #{version.inspect}. " \
170
+ "Must be a semver string (e.g. '2.1.92'), optional pre-release suffix, or a channel token ('latest', 'stable')."
171
+ end
172
+
173
+ # Channel tokens are not concrete versions; skip requirement check.
174
+ return if %w[latest stable].include?(version_str)
175
+
176
+ # Validate concrete versions against the supported range.
177
+ gem_version = Gem::Version.new(version_str)
178
+ return if SUPPORTED_CLI_REQUIREMENT.satisfied_by?(gem_version)
179
+
180
+ raise ArgumentError, "Version #{version.inspect} is outside the supported range " \
181
+ "(#{SUPPORTED_CLI_REQUIREMENT}). Update SUPPORTED_CLI_REQUIREMENT before targeting this version."
182
+ end
183
+
90
184
  def parse_models_list(output)
91
185
  return [] if output.nil? || output.empty?
92
186
 
@@ -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,17 @@ module AgentHarness
53
62
  ]
54
63
  end
55
64
 
56
- def installation_contract
57
- default_package = "@openai/codex@#{SUPPORTED_CLI_VERSION}".freeze
65
+ def installation_contract(version: SUPPORTED_CLI_VERSION)
66
+ unless SUPPORTED_CLI_REQUIREMENT.satisfied_by?(Gem::Version.new(version))
67
+ raise ArgumentError,
68
+ "Unsupported Codex CLI version #{version.inspect}; " \
69
+ "supported versions must satisfy #{SUPPORTED_CLI_REQUIREMENT}"
70
+ end
71
+
72
+ default_package = "@openai/codex@#{version}".freeze
58
73
  install_command_prefix = ["npm", "install", "-g", "--ignore-scripts"].freeze
59
74
  install_command = (install_command_prefix + [default_package]).freeze
60
- supported_versions = [SUPPORTED_CLI_VERSION].freeze
75
+ supported_versions = [version].freeze
61
76
  version_requirement = SUPPORTED_CLI_REQUIREMENT.requirements
62
77
  .map { |op, ver| "#{op} #{ver}".freeze }
63
78
  .freeze
@@ -66,7 +81,7 @@ module AgentHarness
66
81
  source: :npm,
67
82
  package: default_package,
68
83
  package_name: "@openai/codex",
69
- version: SUPPORTED_CLI_VERSION,
84
+ version: version,
70
85
  version_requirement: version_requirement,
71
86
  binary_name: binary_name,
72
87
  install_command_prefix: install_command_prefix,
@@ -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
@@ -253,7 +325,7 @@ module AgentHarness
253
325
  return nil unless self.class.available?
254
326
 
255
327
  begin
256
- result = @executor.execute(["cursor-agent", "mcp", "list"], timeout: 5)
328
+ result = @executor.execute([self.class.binary_name, "mcp", "list"], timeout: 5)
257
329
  return nil unless result.success?
258
330
 
259
331
  parse_mcp_servers_output(result.stdout)
@@ -30,6 +30,15 @@ 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)
34
43
  parsed_version = begin
35
44
  Gem::Version.new(version)
@@ -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: [
@@ -20,6 +20,15 @@ module AgentHarness
20
20
  !!executor.which(binary_name)
21
21
  end
22
22
 
23
+ def provider_metadata_overrides
24
+ {
25
+ auth: {
26
+ service: :mistral,
27
+ api_family: :mistral
28
+ }
29
+ }
30
+ end
31
+
23
32
  def firewall_requirements
24
33
  {
25
34
  domains: [
@@ -29,6 +29,15 @@ module AgentHarness
29
29
  !!executor.which(binary_name)
30
30
  end
31
31
 
32
+ def provider_metadata_overrides
33
+ {
34
+ auth: {
35
+ service: :openai,
36
+ api_family: :openai_compatible
37
+ }
38
+ }
39
+ end
40
+
32
41
  def firewall_requirements
33
42
  {
34
43
  domains: [