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.238"
- 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
442
|
# File 'lib/agent_harness/providers/anthropic.rb', line 442
def api_key_env_var_names = ["ANTHROPIC_API_KEY"]
|
#api_key_unset_vars ⇒ Object
444
|
# File 'lib/agent_harness/providers/anthropic.rb', line 444
def api_key_unset_vars = ["ANTHROPIC_BASE_URL", "ANTHROPIC_HEADER_X_AGENT_RUN_ID", "ANTHROPIC_HEADER_X_PROXY_TOKEN"]
|
#auth_type ⇒ Object
518
519
520
|
# File 'lib/agent_harness/providers/anthropic.rb', line 518
def auth_type
:oauth
end
|
#build_mcp_flags(mcp_servers, working_dir: nil) ⇒ Object
475
476
477
478
|
# File 'lib/agent_harness/providers/anthropic.rb', line 475
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
502
503
504
505
506
507
508
|
# File 'lib/agent_harness/providers/anthropic.rb', line 502
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
406
407
408
409
410
411
412
413
414
415
416
|
# File 'lib/agent_harness/providers/anthropic.rb', line 406
def capabilities
{
streaming: true,
file_upload: true,
vision: true,
tool_use: true,
json_mode: true,
mcp: true,
dangerous_mode: true
}
end
|
#chat_models ⇒ Object
494
495
496
|
# File 'lib/agent_harness/providers/anthropic.rb', line 494
def chat_models
CHAT_MODELS
end
|
#chat_transport ⇒ Object
498
499
500
|
# File 'lib/agent_harness/providers/anthropic.rb', line 498
def chat_transport
@chat_transport ||= TextTransport.new(api_key: resolve_text_mode_api_key, logger: @logger)
end
|
#chat_transport_type ⇒ Object
510
511
512
|
# File 'lib/agent_harness/providers/anthropic.rb', line 510
def chat_transport_type
:anthropic
end
|
#configuration_schema ⇒ Object
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
|
# File 'lib/agent_harness/providers/anthropic.rb', line 389
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
514
515
516
|
# File 'lib/agent_harness/providers/anthropic.rb', line 514
def dangerous_mode_flags
["--dangerously-skip-permissions"]
end
|
#display_name ⇒ Object
385
386
387
|
# File 'lib/agent_harness/providers/anthropic.rb', line 385
def display_name
"Anthropic Claude CLI"
end
|
#error_classification_patterns ⇒ Object
596
597
598
599
600
601
602
603
|
# File 'lib/agent_harness/providers/anthropic.rb', line 596
def error_classification_patterns
super.merge(
abort: [
/free tier limit reached/i,
/please upgrade to a paid plan/i
]
)
end
|
#error_patterns ⇒ Object
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
|
# File 'lib/agent_harness/providers/anthropic.rb', line 549
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
536
537
538
539
540
541
542
543
544
545
546
547
|
# File 'lib/agent_harness/providers/anthropic.rb', line 536
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
605
606
607
608
609
610
611
612
613
614
615
616
617
|
# File 'lib/agent_harness/providers/anthropic.rb', line 605
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
381
382
383
|
# File 'lib/agent_harness/providers/anthropic.rb', line 381
def name
"anthropic"
end
|
#plan_execution(prompt:, **options) ⇒ Object
431
432
433
434
435
436
437
438
439
440
|
# File 'lib/agent_harness/providers/anthropic.rb', line 431
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
418
419
420
421
422
423
424
425
426
427
428
429
|
# File 'lib/agent_harness/providers/anthropic.rb', line 418
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
446
|
# File 'lib/agent_harness/providers/anthropic.rb', line 446
def subscription_unset_vars = ["ANTHROPIC_API_KEY", "ANTHROPIC_BASE_URL"] + api_key_unset_vars
|
#supported_mcp_transports ⇒ Object
471
472
473
|
# File 'lib/agent_harness/providers/anthropic.rb', line 471
def supported_mcp_transports
%w[stdio http sse]
end
|
#supports_chat? ⇒ Boolean
490
491
492
|
# File 'lib/agent_harness/providers/anthropic.rb', line 490
def supports_chat?
true
end
|
#supports_mcp? ⇒ Boolean
467
468
469
|
# File 'lib/agent_harness/providers/anthropic.rb', line 467
def supports_mcp?
true
end
|
#supports_text_mode? ⇒ Boolean
484
485
486
|
# File 'lib/agent_harness/providers/anthropic.rb', line 484
def supports_text_mode?
true
end
|
#supports_token_counting? ⇒ Boolean
522
523
524
|
# File 'lib/agent_harness/providers/anthropic.rb', line 522
def supports_token_counting?
true
end
|
480
481
482
|
# File 'lib/agent_harness/providers/anthropic.rb', line 480
def supports_tool_control?
true
end
|
#token_usage_from_api_response(body) ⇒ Object
526
527
528
529
530
531
532
533
534
|
# File 'lib/agent_harness/providers/anthropic.rb', line 526
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.
453
454
455
456
457
458
459
460
461
462
463
464
465
|
# File 'lib/agent_harness/providers/anthropic.rb', line 453
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
|