Class: AgentHarness::Providers::Opencode
- Inherits:
-
Base
- Object
- Base
- AgentHarness::Providers::Opencode
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_EXTERNAL_DIRECTORY_PATTERNS =
Allowlist of external_directory patterns auto-approved in
non-interactive execution. See the DEFAULT_PERMISSION_RULE comment
below for the rationale (precedent: #289/#282/#277/#280).
["/tmp/**", "/home/agent/**"].freeze
- DEFAULT_PERMISSION_CONFIG =
{
"external_directory" => DEFAULT_PERMISSION_EXTERNAL_DIRECTORY_PATTERNS
.to_h { |pattern| [pattern, "allow"] }
.freeze
}.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_RULE =
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 dedicated
read/write/edit tool calls targeting paths outside the project dir are
auto-rejected and the agent silently loses access to its own scratch
output (or its own config/data files) and never completes the task.
This default rule broadens the allowlist (see
DEFAULT_PERMISSION_EXTERNAL_DIRECTORY_PATTERNS) to all of /tmp (so
the agent can read back files it or its sub-agents wrote there) and to
the full agent home directory (so the agent can inspect/maintain its
own config, cache, and data files such as ~/.config/opencode,
~/.local/share/opencode, ~/.cache). The container is already isolated
(Docker, non-root user) and the agent runs with --auto which approves
everything inside the project dir. See #289 (precedent: #282/#277/#280).
{
"external_directory" => DEFAULT_PERMISSION_EXTERNAL_DIRECTORY_PATTERNS
.to_h { |pattern| [pattern, "allow"] }
.freeze
}.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
#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!
Class Method Details
.binary_name ⇒ Object
36
37
38
|
# File 'lib/agent_harness/providers/opencode.rb', line 36
def binary_name
"opencode"
end
|
.discover_models ⇒ Object
67
68
69
70
|
# File 'lib/agent_harness/providers/opencode.rb', line 67
def discover_models
return [] unless available?
[]
end
|
.firewall_requirements ⇒ Object
54
55
56
57
58
59
60
61
|
# File 'lib/agent_harness/providers/opencode.rb', line 54
def firewall_requirements
{
domains: [
"api.openai.com"
],
ip_ranges: []
}
end
|
.install_command(version: SUPPORTED_CLI_VERSION) ⇒ Object
79
80
81
|
# File 'lib/agent_harness/providers/opencode.rb', line 79
def install_command(version: SUPPORTED_CLI_VERSION)
installation_contract(version: version)[:install_command]
end
|
.installation_contract(version: SUPPORTED_CLI_VERSION) ⇒ Object
.instruction_file_paths ⇒ Object
63
64
65
|
# File 'lib/agent_harness/providers/opencode.rb', line 63
def instruction_file_paths
[]
end
|
45
46
47
48
49
50
51
52
|
# File 'lib/agent_harness/providers/opencode.rb', line 45
def provider_metadata_overrides
{
auth: {
service: :openai,
api_family: :openai_compatible
}
}
end
|
.provider_name ⇒ Object
32
33
34
|
# File 'lib/agent_harness/providers/opencode.rb', line 32
def provider_name
:opencode
end
|
.smoke_test_contract ⇒ Object
Instance Method Details
#capabilities ⇒ Object
174
175
176
177
178
179
180
181
182
183
184
|
# File 'lib/agent_harness/providers/opencode.rb', line 174
def capabilities
{
streaming: false,
file_upload: false,
vision: false,
tool_use: false,
json_mode: false,
mcp: false,
dangerous_mode: false
}
end
|
#configuration_schema ⇒ Object
166
167
168
169
170
171
172
|
# File 'lib/agent_harness/providers/opencode.rb', line 166
def configuration_schema
{
fields: [],
auth_modes: [:api_key],
openai_compatible: true
}
end
|
#display_name ⇒ Object
162
163
164
|
# File 'lib/agent_harness/providers/opencode.rb', line 162
def display_name
"OpenCode CLI"
end
|
#error_patterns ⇒ Object
219
220
221
|
# File 'lib/agent_harness/providers/opencode.rb', line 219
def error_patterns
COMMON_ERROR_PATTERNS
end
|
#execution_semantics ⇒ Object
223
224
225
226
227
228
229
230
231
232
233
234
|
# File 'lib/agent_harness/providers/opencode.rb', line 223
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
190
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
216
217
|
# File 'lib/agent_harness/providers/opencode.rb', line 190
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
|
#name ⇒ Object
158
159
160
|
# File 'lib/agent_harness/providers/opencode.rb', line 158
def name
"opencode"
end
|
#supports_activity_heartbeat? ⇒ Boolean
186
187
188
|
# File 'lib/agent_harness/providers/opencode.rb', line 186
def supports_activity_heartbeat?
true
end
|