Class: AgentHarness::Providers::Anthropic
Overview
Anthropic Claude Code CLI provider
Provides integration with the Claude Code CLI tool for AI-powered
coding assistance.
Constant Summary
collapse
"anthropic-ratelimit-tokens-limit"
- RATE_LIMIT_HEADER_REMAINING =
"anthropic-ratelimit-tokens-remaining"
"anthropic-ratelimit-tokens-reset"
- MODEL_PATTERN =
Model name pattern for Anthropic Claude models
/^claude-[\d.-]+-(?:opus|sonnet|haiku)(?:-\d{8})?$/i
- SUPPORTED_CLI_VERSION =
"2.1.92"
- SUPPORTED_CLI_REQUIREMENT =
Gem::Requirement.new(">= #{SUPPORTED_CLI_VERSION}", "< 2.2.0").freeze
- VALID_VERSION_PATTERN =
Matches semver (e.g. "2.1.92"), optional pre-release (e.g. "2.1.92-beta.1"),
or channel tokens (e.g. "latest", "stable").
/\A(?:\d+\.\d+\.\d+(?:-[a-zA-Z0-9.]+)?|latest|stable)\z/
- CHAT_MODELS =
%w[claude-sonnet-4-20250514 claude-haiku-4-20250414 claude-opus-4-20250514].freeze
Constants inherited
from Base
Base::COMMON_ERROR_PATTERNS, Base::DEFAULT_SMOKE_TEST_CONTRACT
Instance Attribute Summary
Attributes inherited from Base
#config, #executor, #logger
Class Method Summary
collapse
Instance Method Summary
collapse
#cleanup_mcp_tempfiles!, #write_mcp_config_file
#parse_rate_limit_reset
Methods inherited from Base
#check_quota, #cli_env_overrides, #configure, #initialize, #parse_container_output, #parse_test_error, #preflight_check, #sandboxed_environment?, #send_chat_message, #test_command_overrides
Methods included from Adapter
#auth_lock_config, #config_file_content, #health_status, #heartbeat_integration, included, metadata_package_name, #noisy_error_patterns, normalize_metadata_installation, normalize_metadata_runtime_requirements, normalize_metadata_source_type, normalize_metadata_version_requirement, #notify_hook_content, #parse_rate_limit_reset, #preflight_check, #session_flags, #smoke_test, #smoke_test_contract, #supports_activity_heartbeat?, #supports_dangerous_mode?, #supports_message_tool_injection?, #supports_sessions?, #translate_error, #validate_config, #validate_mcp_servers!
Class Method Details
.binary_name ⇒ Object
42
43
44
|
# File 'lib/agent_harness/providers/anthropic.rb', line 42
def binary_name
"claude"
end
|
.discover_models ⇒ Object
141
142
143
144
145
146
147
148
149
150
151
152
153
154
|
# File 'lib/agent_harness/providers/anthropic.rb', line 141
def discover_models
return [] unless available?
begin
require "open3"
output, _, status = Open3.capture3("claude", "models", "list", {timeout: 10})
return [] unless status.success?
parse_models_list(output)
rescue => e
AgentHarness.logger&.debug("[AgentHarness::Anthropic] Model discovery failed: #{e.message}")
[]
end
end
|
.firewall_requirements ⇒ Object
120
121
122
123
124
125
126
127
128
129
|
# File 'lib/agent_harness/providers/anthropic.rb', line 120
def firewall_requirements
{
domains: [
"api.anthropic.com",
"claude.ai",
"console.anthropic.com"
],
ip_ranges: []
}
end
|
.install_contract(version: nil) ⇒ Object
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
|
# File 'lib/agent_harness/providers/anthropic.rb', line 46
def install_contract(version: nil)
target_version = version.nil? ? SUPPORTED_CLI_VERSION : version
target_version = target_version.strip if target_version.respond_to?(:strip)
validate_version!(target_version)
version_requirement = SUPPORTED_CLI_REQUIREMENT.requirements
.map { |op, ver| "#{op} #{ver}" }
.join(", ")
channel_token = %w[latest stable].include?(target_version.to_s)
warning = "Review the downloaded installer before execution and verify any published checksum or signature metadata when available."
if channel_token
warning += " Channel '#{target_version}' is not pinned; the resolved version may fall " \
"outside the supported range (#{version_requirement}). Verify the installed version " \
"after installation."
end
{
provider: provider_name,
binary_name: binary_name,
binary_paths: [
"$HOME/.local/bin/claude",
binary_name
],
install: {
strategy: :shell,
source: "official",
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)}",
warning: warning,
post_install_binary_path: "$HOME/.local/bin/claude",
version_not_pinned: channel_token
},
supported_versions: {
default: SUPPORTED_CLI_VERSION,
requirement: version_requirement,
channel: "stable"
},
runtime_contract: {
available_via: binary_name,
build_command: [
binary_name,
"--print",
"--output-format=json"
],
required_features: [
"print_mode",
"json_output",
"mcp_config",
"mcp_list",
"dangerously_skip_permissions",
"models_list"
]
}
}
end
|
.instruction_file_paths ⇒ Object
131
132
133
134
135
136
137
138
139
|
# File 'lib/agent_harness/providers/anthropic.rb', line 131
def instruction_file_paths
[
{
path: "CLAUDE.md",
description: "Claude Code CLI agent instructions",
symlink: true
}
]
end
|
.model_family(provider_model_name) ⇒ Object
Normalize a provider-specific model name to its model family
157
158
159
|
# File 'lib/agent_harness/providers/anthropic.rb', line 157
def model_family(provider_model_name)
provider_model_name.sub(/-\d{8}$/, "")
end
|
.parse_cli_json_envelope(json_string) ⇒ Hash?
Parse a raw Claude CLI --output-format=json envelope into its components.
Downstream callers that capture Claude CLI stdout directly (e.g. container
execution plans) can use this to extract the assistant text, error state,
token usage, and structured metadata without re-implementing the parsing.
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
|
# File 'lib/agent_harness/providers/anthropic.rb', line 191
def parse_cli_json_envelope(json_string)
return nil if json_string.nil? || json_string.empty?
cleaned = json_string.lines.reject { |line|
line.include?('"type":"session.') || line.include?('"type": "session.')
}.join.strip
return nil if cleaned.empty?
parsed = JSON.parse(cleaned)
return nil unless parsed.is_a?(Hash) && parsed.key?("result")
output = parsed["result"]
error = nil
if parsed["is_error"]
error = classify_error_message(output || "Unknown Claude CLI error")
end
tokens = (parsed)
metadata = (parsed)
{output: output, error: error, tokens: tokens, metadata: metadata}
rescue JSON::ParserError
nil
end
|
108
109
110
111
112
113
114
115
116
117
118
|
# File 'lib/agent_harness/providers/anthropic.rb', line 108
def provider_metadata_overrides
{
auth: {
service: :anthropic,
api_family: :anthropic
},
identity: {
bot_usernames: %w[claude anthropic]
}
}
end
|
.provider_model_name(family_name) ⇒ Object
Convert a model family name to the provider's preferred model name
162
163
164
|
# File 'lib/agent_harness/providers/anthropic.rb', line 162
def provider_model_name(family_name)
family_name
end
|
.provider_name ⇒ Object
38
39
40
|
# File 'lib/agent_harness/providers/anthropic.rb', line 38
def provider_name
:claude
end
|
.smoke_test_contract ⇒ Object
.supports_chat? ⇒ Boolean
171
172
173
|
# File 'lib/agent_harness/providers/anthropic.rb', line 171
def supports_chat?
true
end
|
.supports_model_family?(family_name) ⇒ Boolean
Check if this provider supports a given model family
167
168
169
|
# File 'lib/agent_harness/providers/anthropic.rb', line 167
def supports_model_family?(family_name)
MODEL_PATTERN.match?(family_name)
end
|
Instance Method Details
#api_key_env_var_names ⇒ Object
416
|
# File 'lib/agent_harness/providers/anthropic.rb', line 416
def api_key_env_var_names = ["ANTHROPIC_API_KEY"]
|
#api_key_unset_vars ⇒ Object
418
|
# File 'lib/agent_harness/providers/anthropic.rb', line 418
def api_key_unset_vars = ["ANTHROPIC_BASE_URL", "ANTHROPIC_HEADER_X_AGENT_RUN_ID", "ANTHROPIC_HEADER_X_PROXY_TOKEN"]
|
#auth_type ⇒ Object
492
493
494
|
# File 'lib/agent_harness/providers/anthropic.rb', line 492
def auth_type
:oauth
end
|
#build_mcp_flags(mcp_servers, working_dir: nil) ⇒ Object
449
450
451
452
|
# File 'lib/agent_harness/providers/anthropic.rb', line 449
def build_mcp_flags(mcp_servers, working_dir: nil)
config_path = write_mcp_config_file(mcp_servers, working_dir: working_dir)
["--mcp-config=#{config_path}"]
end
|
#build_runtime_chat_transport(runtime) ⇒ Object
476
477
478
479
480
481
482
|
# File 'lib/agent_harness/providers/anthropic.rb', line 476
def build_runtime_chat_transport(runtime)
TextTransport.new(
base_url: runtime.chat_base_url || TextTransport::ANTHROPIC_API_URL,
api_key: runtime.chat_api_key || resolve_text_mode_api_key,
logger: @logger
)
end
|
#capabilities ⇒ Object
380
381
382
383
384
385
386
387
388
389
390
|
# File 'lib/agent_harness/providers/anthropic.rb', line 380
def capabilities
{
streaming: true,
file_upload: true,
vision: true,
tool_use: true,
json_mode: true,
mcp: true,
dangerous_mode: true
}
end
|
#chat_models ⇒ Object
468
469
470
|
# File 'lib/agent_harness/providers/anthropic.rb', line 468
def chat_models
CHAT_MODELS
end
|
#chat_transport ⇒ Object
472
473
474
|
# File 'lib/agent_harness/providers/anthropic.rb', line 472
def chat_transport
@chat_transport ||= TextTransport.new(api_key: resolve_text_mode_api_key, logger: @logger)
end
|
#chat_transport_type ⇒ Object
484
485
486
|
# File 'lib/agent_harness/providers/anthropic.rb', line 484
def chat_transport_type
:anthropic
end
|
#configuration_schema ⇒ Object
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
|
# File 'lib/agent_harness/providers/anthropic.rb', line 363
def configuration_schema
{
fields: [
{
name: :model,
type: :string,
label: "Model",
required: false,
hint: "Claude model to use (e.g. claude-3-5-sonnet-20241022)",
accepts_arbitrary: false
}
],
auth_modes: [:oauth],
openai_compatible: false
}
end
|
#dangerous_mode_flags ⇒ Object
488
489
490
|
# File 'lib/agent_harness/providers/anthropic.rb', line 488
def dangerous_mode_flags
["--dangerously-skip-permissions"]
end
|
#display_name ⇒ Object
359
360
361
|
# File 'lib/agent_harness/providers/anthropic.rb', line 359
def display_name
"Anthropic Claude CLI"
end
|
#error_classification_patterns ⇒ Object
570
571
572
573
574
575
576
577
|
# File 'lib/agent_harness/providers/anthropic.rb', line 570
def error_classification_patterns
super.merge(
abort: [
/free tier limit reached/i,
/please upgrade to a paid plan/i
]
)
end
|
#error_patterns ⇒ Object
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
|
# File 'lib/agent_harness/providers/anthropic.rb', line 523
def error_patterns
{
rate_limited: [
/rate.?limit/i,
/too.?many.?requests/i,
/\b429\b/,
/overloaded/i,
/session.?limit/i
],
auth_expired: [
/oauth.*token.*expired/i,
/authentication.*error/i,
/invalid.*api.*key/i,
/unauthorized/i,
/\b401\b/,
/session.*expired/i,
/not.*logged.*in/i,
/login.*required/i,
/credentials.*expired/i
],
quota_exceeded: [
/quota.*exceeded/i,
/usage.*limit/i,
/credit.*exhausted/i
],
transient: [
/timeout/i,
/connection.*reset/i,
/temporary.*error/i,
/service.*unavailable/i,
/\b503\b/,
/\b502\b/,
/\b504\b/
],
permanent: [
/invalid.*model/i,
/unsupported.*operation/i,
/not.*found/i,
/\b404\b/,
/bad.*request/i,
/\b400\b/,
/model.*deprecated/i,
/end-of-life/i
]
}
end
|
#execution_semantics ⇒ Object
510
511
512
513
514
515
516
517
518
519
520
521
|
# File 'lib/agent_harness/providers/anthropic.rb', line 510
def execution_semantics
{
prompt_delivery: :arg,
output_format: :json,
sandbox_aware: true,
uses_subcommand: false,
non_interactive_flag: "--print",
legitimate_exit_codes: [0],
stderr_is_diagnostic: true,
parses_rate_limit_reset: false
}
end
|
#fetch_mcp_servers ⇒ Object
579
580
581
582
583
584
585
586
587
588
589
590
591
|
# File 'lib/agent_harness/providers/anthropic.rb', line 579
def fetch_mcp_servers
return [] unless self.class.available?
begin
result = @executor.execute(["claude", "mcp", "list"], timeout: 5)
return [] unless result.success?
parse_claude_mcp_output(result.stdout)
rescue => e
log_debug("fetch_mcp_servers_failed", error: e.message)
[]
end
end
|
#name ⇒ Object
355
356
357
|
# File 'lib/agent_harness/providers/anthropic.rb', line 355
def name
"anthropic"
end
|
#plan_execution(prompt:, **options) ⇒ Object
405
406
407
408
409
410
411
412
413
414
|
# File 'lib/agent_harness/providers/anthropic.rb', line 405
def plan_execution(prompt:, **options)
if options[:mode] == :text
raise ProviderError,
"Anthropic text mode uses the HTTP transport and does not produce a CLI execution plan"
end
super
ensure
cleanup_mcp_tempfiles!
end
|
#send_message(prompt:, **options) ⇒ Object
392
393
394
395
396
397
398
399
400
401
402
403
|
# File 'lib/agent_harness/providers/anthropic.rb', line 392
def send_message(prompt:, **options)
if options[:mode] == :text
options = normalize_provider_runtime(options)
skill_context = resolve_skills(options)
prompt = apply_skills_to_prompt(prompt, skill_context)
return send_text_message(prompt, **skill_context[:options].except(:mode, :skills))
end
super
ensure
cleanup_mcp_tempfiles!
end
|
#subscription_unset_vars ⇒ Object
420
|
# File 'lib/agent_harness/providers/anthropic.rb', line 420
def subscription_unset_vars = ["ANTHROPIC_API_KEY", "ANTHROPIC_BASE_URL"] + api_key_unset_vars
|
#supported_mcp_transports ⇒ Object
445
446
447
|
# File 'lib/agent_harness/providers/anthropic.rb', line 445
def supported_mcp_transports
%w[stdio http sse]
end
|
#supports_chat? ⇒ Boolean
464
465
466
|
# File 'lib/agent_harness/providers/anthropic.rb', line 464
def supports_chat?
true
end
|
#supports_mcp? ⇒ Boolean
441
442
443
|
# File 'lib/agent_harness/providers/anthropic.rb', line 441
def supports_mcp?
true
end
|
#supports_text_mode? ⇒ Boolean
458
459
460
|
# File 'lib/agent_harness/providers/anthropic.rb', line 458
def supports_text_mode?
true
end
|
#supports_token_counting? ⇒ Boolean
496
497
498
|
# File 'lib/agent_harness/providers/anthropic.rb', line 496
def supports_token_counting?
true
end
|
454
455
456
|
# File 'lib/agent_harness/providers/anthropic.rb', line 454
def supports_tool_control?
true
end
|
#token_usage_from_api_response(body) ⇒ Object
500
501
502
503
504
505
506
507
508
|
# File 'lib/agent_harness/providers/anthropic.rb', line 500
def token_usage_from_api_response(body)
usage = body&.dig("usage")
return {} unless usage
{
input_tokens: usage["input_tokens"].to_i,
output_tokens: usage["output_tokens"].to_i
}
end
|
Opportunistically refresh quota info from Anthropic rate-limit headers
observed on a normal /v1/messages response.
427
428
429
430
431
432
433
434
435
436
437
438
439
|
# File 'lib/agent_harness/providers/anthropic.rb', line 427
def ()
limit_value = (, RATE_LIMIT_HEADER_LIMIT)
remaining_value = (, RATE_LIMIT_HEADER_REMAINING)
return nil unless limit_value || remaining_value
QuotaStatus.new(
available: true,
remaining: remaining_value&.to_i,
limit: limit_value&.to_i,
reset_at: (),
unit: :tokens
)
end
|