agent-cli-runtime 0.1.0 → 0.2.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -0
- data/README.md +201 -41
- data/agent-cli-runtime.gemspec +7 -7
- data/lib/agent_cli_runtime/errors.rb +7 -0
- data/lib/agent_cli_runtime/opencode/inspection.rb +32 -0
- data/lib/agent_cli_runtime/opencode/overlay.rb +573 -0
- data/lib/agent_cli_runtime/opencode/probe.rb +280 -0
- data/lib/agent_cli_runtime/opencode/result_parser.rb +402 -0
- data/lib/agent_cli_runtime/profile.rb +96 -2
- data/lib/agent_cli_runtime/profiles.rb +131 -1
- data/lib/agent_cli_runtime/runtime.rb +18 -3
- data/lib/agent_cli_runtime/values.rb +437 -3
- data/lib/agent_cli_runtime/version.rb +1 -1
- data/lib/agent_cli_runtime.rb +23 -3
- metadata +13 -8
|
@@ -25,7 +25,10 @@ module AgentCliRuntime
|
|
|
25
25
|
:budget_flag, :output_format_flags, :version_flag,
|
|
26
26
|
:min_version, :prompt_style, :model_argument_builder,
|
|
27
27
|
:effort_argument_builder, :launcher_identity,
|
|
28
|
-
:cli_capabilities, :declared_capability_support
|
|
28
|
+
:cli_capabilities, :declared_capability_support,
|
|
29
|
+
:credential_environment_keys, :configuration_environment_key,
|
|
30
|
+
:default_configuration_directory,
|
|
31
|
+
:permission_policy_required, :result_parser
|
|
29
32
|
|
|
30
33
|
def initialize(name:, bin_default:, headless_flag:, version_flag:,
|
|
31
34
|
env_bin_override_keys: [], permission_skip_flag: nil,
|
|
@@ -35,7 +38,10 @@ module AgentCliRuntime
|
|
|
35
38
|
prompt_style: :positional, model_argument_builder: nil,
|
|
36
39
|
effort_argument_builder: nil, launcher_identity: nil,
|
|
37
40
|
usage_extractor: nil, auth_configuration_probe: nil,
|
|
38
|
-
cli_capabilities: {}, raw_cli_arguments_supported: false
|
|
41
|
+
cli_capabilities: {}, raw_cli_arguments_supported: false,
|
|
42
|
+
credential_environment_keys: [], configuration_environment_key: nil,
|
|
43
|
+
default_configuration_directory: nil,
|
|
44
|
+
permission_policy_required: false, result_parser: nil)
|
|
39
45
|
normalized_prompt_style = prompt_style.to_sym
|
|
40
46
|
unless PROMPT_STYLES.include?(normalized_prompt_style)
|
|
41
47
|
raise ArgumentError,
|
|
@@ -65,6 +71,17 @@ module AgentCliRuntime
|
|
|
65
71
|
@auth_configuration_probe = auth_configuration_probe
|
|
66
72
|
@cli_capabilities = normalize_cli_capabilities(cli_capabilities)
|
|
67
73
|
@raw_cli_arguments_supported = raw_cli_arguments_supported == true
|
|
74
|
+
@credential_environment_keys = immutable_environment_keys(
|
|
75
|
+
credential_environment_keys
|
|
76
|
+
)
|
|
77
|
+
@configuration_environment_key = optional_environment_key(
|
|
78
|
+
configuration_environment_key
|
|
79
|
+
)
|
|
80
|
+
@default_configuration_directory = optional_relative_directory(
|
|
81
|
+
default_configuration_directory
|
|
82
|
+
)
|
|
83
|
+
@permission_policy_required = permission_policy_required == true
|
|
84
|
+
@result_parser = result_parser
|
|
68
85
|
@declared_capability_support = build_declared_capability_support
|
|
69
86
|
freeze
|
|
70
87
|
end
|
|
@@ -89,6 +106,10 @@ module AgentCliRuntime
|
|
|
89
106
|
raise ArgumentError,
|
|
90
107
|
"agent profile #{@name.inspect} cannot enforce read-only sandboxing"
|
|
91
108
|
end
|
|
109
|
+
if permission_mode.nil? && @permission_policy_required
|
|
110
|
+
raise ConfigurationError,
|
|
111
|
+
"agent profile #{@name.inspect} requires an explicit OpenCode permission policy"
|
|
112
|
+
end
|
|
92
113
|
return [] unless @permission_skip_flag
|
|
93
114
|
return [ @permission_skip_flag ] unless @name == :claude && permission_mode
|
|
94
115
|
return [ @permission_skip_flag ] if permission_mode == "bypassPermissions"
|
|
@@ -200,6 +221,44 @@ module AgentCliRuntime
|
|
|
200
221
|
nil
|
|
201
222
|
end
|
|
202
223
|
|
|
224
|
+
def parse_run(stdout)
|
|
225
|
+
unless @result_parser
|
|
226
|
+
raise UnsupportedCapability,
|
|
227
|
+
"agent profile #{@name.inspect} has no strict result parser"
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
@result_parser.parse_run(stdout)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def normalize_captured_result(captured, requested_route:)
|
|
234
|
+
unless @result_parser
|
|
235
|
+
raise UnsupportedCapability,
|
|
236
|
+
"agent profile #{@name.inspect} has no strict result parser"
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
@result_parser.normalize(
|
|
240
|
+
captured, requested_route: requested_route, profile: self
|
|
241
|
+
)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def configuration_directory(home: nil, env: ENV)
|
|
245
|
+
if @configuration_environment_key
|
|
246
|
+
configured = env[@configuration_environment_key].to_s
|
|
247
|
+
unless configured.empty?
|
|
248
|
+
unless File.absolute_path?(configured)
|
|
249
|
+
raise ArgumentError,
|
|
250
|
+
"#{@configuration_environment_key} must be an absolute directory"
|
|
251
|
+
end
|
|
252
|
+
return File.expand_path(configured)
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
return nil unless @default_configuration_directory
|
|
256
|
+
|
|
257
|
+
base = home || env["HOME"]
|
|
258
|
+
base = Dir.home if base.to_s.empty?
|
|
259
|
+
File.expand_path(@default_configuration_directory, base)
|
|
260
|
+
end
|
|
261
|
+
|
|
203
262
|
def require_cli_capability!(capability)
|
|
204
263
|
capability_name = capability.to_sym
|
|
205
264
|
flags = @cli_capabilities[capability_name]
|
|
@@ -219,6 +278,12 @@ module AgentCliRuntime
|
|
|
219
278
|
flags.dup
|
|
220
279
|
end
|
|
221
280
|
|
|
281
|
+
def capture_local(*arguments, env: ENV, timeout_sec: CAPTURE_TIMEOUT_SECONDS)
|
|
282
|
+
bounded_capture3(
|
|
283
|
+
bin(env:), *arguments, timeout_sec: timeout_sec, env: env
|
|
284
|
+
)
|
|
285
|
+
end
|
|
286
|
+
|
|
222
287
|
private
|
|
223
288
|
|
|
224
289
|
def immutable_string(value)
|
|
@@ -229,6 +294,35 @@ module AgentCliRuntime
|
|
|
229
294
|
Array(values).map { |value| immutable_string(value) }.freeze
|
|
230
295
|
end
|
|
231
296
|
|
|
297
|
+
def immutable_environment_keys(values)
|
|
298
|
+
keys = immutable_strings(values)
|
|
299
|
+
invalid = keys.find { |key| !key.match?(/\A[A-Z][A-Z0-9_]*\z/) }
|
|
300
|
+
raise ArgumentError, "invalid credential environment key #{invalid.inspect}" if invalid
|
|
301
|
+
raise ArgumentError, "credential environment keys must be unique" if keys.uniq.length != keys.length
|
|
302
|
+
|
|
303
|
+
keys
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def optional_environment_key(value)
|
|
307
|
+
return nil if value.nil?
|
|
308
|
+
|
|
309
|
+
key = immutable_string(value)
|
|
310
|
+
unless key.match?(/\A[A-Z][A-Z0-9_]*\z/)
|
|
311
|
+
raise ArgumentError, "invalid configuration environment key #{key.inspect}"
|
|
312
|
+
end
|
|
313
|
+
key
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def optional_relative_directory(value)
|
|
317
|
+
return nil if value.nil?
|
|
318
|
+
|
|
319
|
+
path = immutable_string(value)
|
|
320
|
+
if path.empty? || File.absolute_path?(path)
|
|
321
|
+
raise ArgumentError, "default configuration directory must be relative"
|
|
322
|
+
end
|
|
323
|
+
path
|
|
324
|
+
end
|
|
325
|
+
|
|
232
326
|
def normalize_tool_scope_flags(flags)
|
|
233
327
|
unless flags.is_a?(Hash)
|
|
234
328
|
raise ArgumentError, "tool_scope_flags must be a Hash"
|
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
module AgentCliRuntime
|
|
2
2
|
module Profiles
|
|
3
|
+
PI_CREDENTIAL_ENVIRONMENT_KEYS = %w[
|
|
4
|
+
AI_GATEWAY_API_KEY
|
|
5
|
+
ANTHROPIC_API_KEY
|
|
6
|
+
ANTHROPIC_AUTH_TOKEN
|
|
7
|
+
ANTHROPIC_OAUTH_TOKEN
|
|
8
|
+
ANT_LING_API_KEY
|
|
9
|
+
AWS_ACCESS_KEY_ID
|
|
10
|
+
AWS_BEARER_TOKEN_BEDROCK
|
|
11
|
+
AWS_CONTAINER_CREDENTIALS_FULL_URI
|
|
12
|
+
AWS_CONTAINER_CREDENTIALS_RELATIVE_URI
|
|
13
|
+
AWS_PROFILE
|
|
14
|
+
AWS_SECRET_ACCESS_KEY
|
|
15
|
+
AWS_SESSION_TOKEN
|
|
16
|
+
AWS_WEB_IDENTITY_TOKEN_FILE
|
|
17
|
+
AZURE_OPENAI_API_KEY
|
|
18
|
+
BASETEN_API_KEY
|
|
19
|
+
CEREBRAS_API_KEY
|
|
20
|
+
CLOUDFLARE_API_KEY
|
|
21
|
+
COPILOT_GITHUB_TOKEN
|
|
22
|
+
DEEPSEEK_API_KEY
|
|
23
|
+
FIREWORKS_API_KEY
|
|
24
|
+
GEMINI_API_KEY
|
|
25
|
+
GOOGLE_APPLICATION_CREDENTIALS
|
|
26
|
+
GOOGLE_CLOUD_API_KEY
|
|
27
|
+
GROQ_API_KEY
|
|
28
|
+
HF_TOKEN
|
|
29
|
+
KIMI_API_KEY
|
|
30
|
+
MINIMAX_API_KEY
|
|
31
|
+
MINIMAX_CN_API_KEY
|
|
32
|
+
MISTRAL_API_KEY
|
|
33
|
+
MOONSHOT_API_KEY
|
|
34
|
+
NVIDIA_API_KEY
|
|
35
|
+
OPENCODE_API_KEY
|
|
36
|
+
OPENAI_API_KEY
|
|
37
|
+
OPENROUTER_API_KEY
|
|
38
|
+
QWEN_TOKEN_PLAN_API_KEY
|
|
39
|
+
RADIUS_API_KEY
|
|
40
|
+
TOGETHER_API_KEY
|
|
41
|
+
XAI_API_KEY
|
|
42
|
+
XIAOMI_API_KEY
|
|
43
|
+
XIAOMI_TOKEN_PLAN_AMS_API_KEY
|
|
44
|
+
XIAOMI_TOKEN_PLAN_CN_API_KEY
|
|
45
|
+
XIAOMI_TOKEN_PLAN_SGP_API_KEY
|
|
46
|
+
ZAI_API_KEY
|
|
47
|
+
ZAI_CODING_CN_API_KEY
|
|
48
|
+
].freeze
|
|
49
|
+
|
|
3
50
|
module_function
|
|
4
51
|
|
|
5
52
|
def names
|
|
@@ -56,6 +103,37 @@ module AgentCliRuntime
|
|
|
56
103
|
home_path(home, ".grok", "auth.json")
|
|
57
104
|
end
|
|
58
105
|
|
|
106
|
+
def opencode_auth_path(home:, env:)
|
|
107
|
+
data_home = env["XDG_DATA_HOME"]
|
|
108
|
+
if !data_home.to_s.empty?
|
|
109
|
+
unless File.absolute_path?(data_home)
|
|
110
|
+
raise ArgumentError, "XDG_DATA_HOME must be absolute"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
return File.join(data_home, "opencode", "auth.json")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
home_path(home, ".local", "share", "opencode", "auth.json")
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def opencode_model_arguments(model)
|
|
120
|
+
route = Route.parse(model)
|
|
121
|
+
[ "--model", route.to_s ]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
OPENCODE_VARIANTS = %w[minimal low medium high xhigh max].freeze
|
|
125
|
+
|
|
126
|
+
def opencode_variant_arguments(variant)
|
|
127
|
+
normalized = variant.to_s
|
|
128
|
+
unless OPENCODE_VARIANTS.include?(normalized)
|
|
129
|
+
raise ArgumentError,
|
|
130
|
+
"unsupported OpenCode variant #{variant.inspect}; expected one of " \
|
|
131
|
+
"#{OPENCODE_VARIANTS.join(', ')}"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
[ "--variant", normalized ]
|
|
135
|
+
end
|
|
136
|
+
|
|
59
137
|
CLAUDE = Profile.new(
|
|
60
138
|
name: :claude,
|
|
61
139
|
bin_default: "claude",
|
|
@@ -78,6 +156,11 @@ module AgentCliRuntime
|
|
|
78
156
|
effort_argument_builder: ->(effort) { [ "--effort", effort ] },
|
|
79
157
|
launcher_identity: "claude-code/v1",
|
|
80
158
|
usage_extractor: UsageExtractors::CLAUDE,
|
|
159
|
+
credential_environment_keys: %w[
|
|
160
|
+
ANTHROPIC_API_KEY ANTHROPIC_AUTH_TOKEN CLAUDE_API_KEY
|
|
161
|
+
],
|
|
162
|
+
configuration_environment_key: "CLAUDE_CONFIG_DIR",
|
|
163
|
+
default_configuration_directory: ".claude",
|
|
81
164
|
raw_cli_arguments_supported: true,
|
|
82
165
|
auth_configuration_probe: lambda do |home:, env:|
|
|
83
166
|
if env_configured?(env, "ANTHROPIC_API_KEY")
|
|
@@ -117,6 +200,9 @@ module AgentCliRuntime
|
|
|
117
200
|
->(effort) { [ "-c", "model_reasoning_effort=#{effort}" ] },
|
|
118
201
|
launcher_identity: "codex-cli/v1",
|
|
119
202
|
usage_extractor: UsageExtractors::CODEX,
|
|
203
|
+
credential_environment_keys: %w[OPENAI_API_KEY],
|
|
204
|
+
configuration_environment_key: "CODEX_HOME",
|
|
205
|
+
default_configuration_directory: ".codex",
|
|
120
206
|
auth_configuration_probe: lambda do |home:, env:|
|
|
121
207
|
if env_configured?(env, "OPENAI_API_KEY")
|
|
122
208
|
AuthConfiguration.new(status: :configured, source: "environment")
|
|
@@ -140,6 +226,9 @@ module AgentCliRuntime
|
|
|
140
226
|
model_argument_builder: ->(model) { [ "--model", model ] },
|
|
141
227
|
launcher_identity: "pi-coding-agent/v1",
|
|
142
228
|
usage_extractor: UsageExtractors::PI,
|
|
229
|
+
credential_environment_keys: PI_CREDENTIAL_ENVIRONMENT_KEYS,
|
|
230
|
+
configuration_environment_key: "PI_CODING_AGENT_DIR",
|
|
231
|
+
default_configuration_directory: ".pi/agent",
|
|
143
232
|
auth_configuration_probe: lambda do |home:, env:|
|
|
144
233
|
directory = env["PI_CODING_AGENT_DIR"]
|
|
145
234
|
path =
|
|
@@ -169,6 +258,9 @@ module AgentCliRuntime
|
|
|
169
258
|
->(effort) { [ "--reasoning-effort", effort ] },
|
|
170
259
|
launcher_identity: "grok-cli/v1",
|
|
171
260
|
usage_extractor: UsageExtractors::GROK,
|
|
261
|
+
credential_environment_keys: %w[GROK_AUTH_PATH XAI_API_KEY GROK_CODE_XAI_API_KEY],
|
|
262
|
+
configuration_environment_key: "GROK_HOME",
|
|
263
|
+
default_configuration_directory: ".grok",
|
|
172
264
|
auth_configuration_probe: lambda do |home:, env:|
|
|
173
265
|
if env_configured?(env, "XAI_API_KEY", "GROK_CODE_XAI_API_KEY")
|
|
174
266
|
AuthConfiguration.new(status: :configured, source: "environment")
|
|
@@ -181,7 +273,45 @@ module AgentCliRuntime
|
|
|
181
273
|
end
|
|
182
274
|
)
|
|
183
275
|
|
|
184
|
-
|
|
276
|
+
OPENCODE = Profile.new(
|
|
277
|
+
name: :opencode,
|
|
278
|
+
bin_default: "opencode",
|
|
279
|
+
env_bin_override_keys: %w[
|
|
280
|
+
AGENT_CLI_RUNTIME_OPENCODE_BIN HIVE_OPENCODE_BIN
|
|
281
|
+
],
|
|
282
|
+
headless_flag: "run",
|
|
283
|
+
output_format_flags: [ "--format", "json" ],
|
|
284
|
+
version_flag: "--version",
|
|
285
|
+
min_version: "1.18.16",
|
|
286
|
+
model_argument_builder: ->(model) { opencode_model_arguments(model) },
|
|
287
|
+
effort_argument_builder:
|
|
288
|
+
->(variant) { opencode_variant_arguments(variant) },
|
|
289
|
+
launcher_identity: "opencode-cli/v1",
|
|
290
|
+
credential_environment_keys: PI_CREDENTIAL_ENVIRONMENT_KEYS,
|
|
291
|
+
configuration_environment_key: "OPENCODE_CONFIG_DIR",
|
|
292
|
+
default_configuration_directory: ".config/opencode",
|
|
293
|
+
permission_policy_required: true,
|
|
294
|
+
result_parser: OpenCode::ResultParser,
|
|
295
|
+
cli_capabilities: {
|
|
296
|
+
json_events: [ "run", "--format" ],
|
|
297
|
+
working_directory: [ "run", "--dir" ],
|
|
298
|
+
model_variant: [ "run", "--variant" ],
|
|
299
|
+
pure: [ "run", "--pure" ],
|
|
300
|
+
sanitized_export: [ "export", "--sanitize" ]
|
|
301
|
+
},
|
|
302
|
+
auth_configuration_probe: lambda do |home:, env:|
|
|
303
|
+
if env_configured?(env, *PI_CREDENTIAL_ENVIRONMENT_KEYS)
|
|
304
|
+
AuthConfiguration.new(status: :configured, source: "environment")
|
|
305
|
+
else
|
|
306
|
+
auth_from_file(
|
|
307
|
+
opencode_auth_path(home:, env:),
|
|
308
|
+
source: "opencode auth.json"
|
|
309
|
+
)
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
PROFILES = [ CLAUDE, CODEX, PI, GROK, OPENCODE ].to_h do |profile|
|
|
185
315
|
[ profile.name, profile ]
|
|
186
316
|
end.freeze
|
|
187
317
|
PROVIDER_ORDER = PROFILES.keys.freeze
|
|
@@ -63,9 +63,13 @@ module AgentCliRuntime
|
|
|
63
63
|
compilation_error!(profile, e)
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
-
def prepare!(profile)
|
|
66
|
+
def prepare!(profile, env: ENV)
|
|
67
|
+
if profile.is_a?(OpenCodePreparationRequest)
|
|
68
|
+
return OpenCode::Overlay.prepare!(profile, env:)
|
|
69
|
+
end
|
|
70
|
+
|
|
67
71
|
resolved = Profiles.resolve(profile)
|
|
68
|
-
result = Probe.call(resolved)
|
|
72
|
+
result = Probe.call(resolved, env:)
|
|
69
73
|
return result if result.ready
|
|
70
74
|
|
|
71
75
|
evidence = unsupported_evidence(
|
|
@@ -116,7 +120,18 @@ module AgentCliRuntime
|
|
|
116
120
|
final_message: raw[:final_message],
|
|
117
121
|
diagnostic: Redactor.diagnostic(
|
|
118
122
|
raw[:error_message] || raw[:limit_text]
|
|
119
|
-
)
|
|
123
|
+
),
|
|
124
|
+
provider_signal: raw[:provider_signal]
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def parse_run(profile, stdout:)
|
|
129
|
+
Profiles.resolve(profile).parse_run(stdout)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def normalize(profile, captured, requested_route:)
|
|
133
|
+
Profiles.resolve(profile).normalize_captured_result(
|
|
134
|
+
captured, requested_route:
|
|
120
135
|
)
|
|
121
136
|
end
|
|
122
137
|
|