Class: AgentHarness::Providers::Opencode

Inherits:
Base
  • Object
show all
Defined in:
lib/agent_harness/providers/opencode.rb

Overview

OpenCode CLI provider

Provides integration with the OpenCode CLI tool.

Constant Summary collapse

CLI_PACKAGE =
"opencode-ai"
SUPPORTED_CLI_VERSION =
"1.3.2"
SUPPORTED_CLI_REQUIREMENT =
Gem::Requirement.new(">= #{SUPPORTED_CLI_VERSION}", "< 1.4.0").freeze
INSTALL_COMMAND_PREFIX =
["npm", "install", "-g", "--ignore-scripts"].freeze
DEFAULT_PERMISSION_CONFIG =
{
  "external_directory" => {
    "/tmp/**" => "allow"
  }
}.freeze
SUPPORTED_CLI_VERSIONS =
[SUPPORTED_CLI_VERSION].freeze
POSTINSTALL_COMMAND =
"node $(npm root -g)/opencode-ai/postinstall.mjs"
VERSION_REQUIREMENT_STRINGS =
SUPPORTED_CLI_REQUIREMENT.requirements
.map { |op, ver| "#{op} #{ver}".freeze }
.freeze
DEFAULT_INSTALLATION_CONTRACT =
build_installation_contract(SUPPORTED_CLI_VERSION)
DEFAULT_PERMISSION_EXTERNAL_DIRECTORY_PATTERN =

OpenCode's stock default only allowlists external_directory access to /tmp/opencode/*, but its own sub-agent delegation pattern (e.g. the Explore-Agent used to summarize large diffs/outputs) writes scratch files to other /tmp/* paths. In non-interactive execution there is no human to approve the resulting permission prompt, so the agent silently loses access to its own scratch output and never completes the task. This default rule broadens the allowlist to all of /tmp so the agent can read back files it (or its sub-agents) wrote there.

"/tmp/**"
DEFAULT_PERMISSION_RULE =
{
  "external_directory" => {DEFAULT_PERMISSION_EXTERNAL_DIRECTORY_PATTERN => "allow"}
}.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

Methods inherited from Base

#api_key_env_var_names, #api_key_unset_vars, #cli_env_overrides, #configure, #initialize, #parse_container_output, #parse_test_error, #plan_execution, #preflight_check, #sandboxed_environment?, #send_chat_message, #send_message, #subscription_unset_vars, #test_command_overrides

Methods included from RateLimitResetParsing

#parse_rate_limit_reset

Methods included from Adapter

#auth_lock_config, #auth_type, #build_mcp_flags, #chat_transport, #chat_transport_type, #config_file_content, #dangerous_mode_flags, #error_classification_patterns, #fetch_mcp_servers, #health_status, included, metadata_package_name, #noisy_error_patterns, normalize_metadata_installation, normalize_metadata_source_type, normalize_metadata_version_requirement, #notify_hook_content, #parse_rate_limit_reset, #plan_execution, #preflight_check, #send_message, #session_flags, #smoke_test, #smoke_test_contract, #supported_mcp_transports, #supports_chat?, #supports_dangerous_mode?, #supports_mcp?, #supports_message_tool_injection?, #supports_sessions?, #supports_text_mode?, #supports_token_counting?, #supports_tool_control?, #token_usage_from_api_response, #translate_error, #validate_config, #validate_mcp_servers!

Constructor Details

This class inherits a constructor from AgentHarness::Providers::Base

Class Method Details

.available?Boolean

Returns:

  • (Boolean)


36
37
38
39
# File 'lib/agent_harness/providers/opencode.rb', line 36

def available?
  executor = AgentHarness.configuration.command_executor
  !!executor.which(binary_name)
end

.binary_nameObject



32
33
34
# File 'lib/agent_harness/providers/opencode.rb', line 32

def binary_name
  "opencode"
end

.discover_modelsObject



63
64
65
66
# File 'lib/agent_harness/providers/opencode.rb', line 63

def discover_models
  return [] unless available?
  []
end

.firewall_requirementsObject



50
51
52
53
54
55
56
57
# File 'lib/agent_harness/providers/opencode.rb', line 50

def firewall_requirements
  {
    domains: [
      "api.openai.com"
    ],
    ip_ranges: []
  }
end

.install_command(version: SUPPORTED_CLI_VERSION) ⇒ Object



75
76
77
# File 'lib/agent_harness/providers/opencode.rb', line 75

def install_command(version: SUPPORTED_CLI_VERSION)
  installation_contract(version: version)[:install_command]
end

.installation_contract(version: SUPPORTED_CLI_VERSION) ⇒ Object



68
69
70
71
72
73
# File 'lib/agent_harness/providers/opencode.rb', line 68

def installation_contract(version: SUPPORTED_CLI_VERSION)
  normalized_version = normalize_install_version(version)
  return DEFAULT_INSTALLATION_CONTRACT if normalized_version == SUPPORTED_CLI_VERSION

  build_installation_contract(normalized_version)
end

.instruction_file_pathsObject



59
60
61
# File 'lib/agent_harness/providers/opencode.rb', line 59

def instruction_file_paths
  []
end

.provider_metadata_overridesObject



41
42
43
44
45
46
47
48
# File 'lib/agent_harness/providers/opencode.rb', line 41

def 
  {
    auth: {
      service: :openai,
      api_family: :openai_compatible
    }
  }
end

.provider_nameObject



28
29
30
# File 'lib/agent_harness/providers/opencode.rb', line 28

def provider_name
  :opencode
end

.smoke_test_contractObject



79
80
81
# File 'lib/agent_harness/providers/opencode.rb', line 79

def smoke_test_contract
  Base::DEFAULT_SMOKE_TEST_CONTRACT
end

Instance Method Details

#capabilitiesObject



160
161
162
163
164
165
166
167
168
169
170
# File 'lib/agent_harness/providers/opencode.rb', line 160

def capabilities
  {
    streaming: false,
    file_upload: false,
    vision: false,
    tool_use: false,
    json_mode: false,
    mcp: false,
    dangerous_mode: false
  }
end

#configuration_schemaObject



152
153
154
155
156
157
158
# File 'lib/agent_harness/providers/opencode.rb', line 152

def configuration_schema
  {
    fields: [],
    auth_modes: [:api_key],
    openai_compatible: true
  }
end

#display_nameObject



148
149
150
# File 'lib/agent_harness/providers/opencode.rb', line 148

def display_name
  "OpenCode CLI"
end

#error_patternsObject



205
206
207
# File 'lib/agent_harness/providers/opencode.rb', line 205

def error_patterns
  COMMON_ERROR_PATTERNS
end

#execution_semanticsObject



209
210
211
212
213
214
215
216
217
218
219
220
# File 'lib/agent_harness/providers/opencode.rb', line 209

def execution_semantics
  {
    prompt_delivery: :arg,
    output_format: :text,
    sandbox_aware: false,
    uses_subcommand: true,
    non_interactive_flag: nil,
    legitimate_exit_codes: [0],
    stderr_is_diagnostic: true,
    parses_rate_limit_reset: true
  }
end

#heartbeat_integration(heartbeat_file_path:) ⇒ Object



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/agent_harness/providers/opencode.rb', line 176

def heartbeat_integration(heartbeat_file_path:)
  unless heartbeat_file_path.is_a?(String) && !heartbeat_file_path.strip.empty?
    raise ArgumentError, "heartbeat_file_path must be a non-empty String"
  end
  unless heartbeat_file_path.start_with?("/")
    raise ArgumentError, "heartbeat_file_path must be an absolute path (got #{heartbeat_file_path.inspect})"
  end

  hook_script = heartbeat_hook_script(heartbeat_file_path)
  config_payload = merge_heartbeat_hooks(hook_script)

  preparation = ExecutionPreparation.new(
    file_writes: [
      {
        path: heartbeat_hook_config_path,
        content: serialize_opencode_config(config_payload),
        mode: 0o600
      }
    ]
  )

  {
    supported: true,
    env: {"OPENCODE_HEARTBEAT_FILE" => heartbeat_file_path},
    preparation: preparation,
    granularity: :tool_call
  }
end

#nameObject



144
145
146
# File 'lib/agent_harness/providers/opencode.rb', line 144

def name
  "opencode"
end

#supports_activity_heartbeat?Boolean

Returns:

  • (Boolean)


172
173
174
# File 'lib/agent_harness/providers/opencode.rb', line 172

def supports_activity_heartbeat?
  true
end