agent-cli-runtime 0.1.1 → 0.2.4

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.
@@ -0,0 +1,304 @@
1
+ require "json"
2
+
3
+ module AgentCliRuntime
4
+ module OpenCode
5
+ module Probe
6
+ REQUIRED_RUN_FLAGS = %w[
7
+ --model --variant --format --dir --pure --auto
8
+ ].freeze
9
+ MODEL_INVENTORY_TIMEOUT_SECONDS = 30
10
+ SAFE_ENVIRONMENT_KEYS = %w[
11
+ HOME LANG LC_ALL LOGNAME PATH SHELL SSL_CERT_DIR SSL_CERT_FILE
12
+ TMPDIR USER
13
+ ].freeze
14
+ ANSI_PATTERN = /\e\[[0-?]*[ -\/]*[@-~]/
15
+ private_constant :SAFE_ENVIRONMENT_KEYS, :ANSI_PATTERN
16
+
17
+ module_function
18
+
19
+ def call(request, env: ENV)
20
+ call!(request, env:)
21
+ rescue Error => e
22
+ profile = Profiles.resolve(request.profile)
23
+ executable = request.executable || profile.bin(env:)
24
+ RouteProbeResult.new(
25
+ provider: profile.name,
26
+ ready: false,
27
+ installed: profile.binary_installed?(env:, executable:),
28
+ executable: executable,
29
+ version: nil,
30
+ minimum_version: profile.min_version,
31
+ auth_configuration: AuthConfiguration.new(status: :not_checked),
32
+ route: request.route,
33
+ route_available: false,
34
+ available_variants: [],
35
+ capability_evidence: [
36
+ CapabilityEvidence.new(
37
+ capability: error_capability(e), supported: false,
38
+ provider: profile.name,
39
+ launcher_identity: profile.launcher_identity,
40
+ diagnostic: Redactor.diagnostic(e)
41
+ )
42
+ ],
43
+ diagnostic: Redactor.diagnostic(e)
44
+ )
45
+ end
46
+
47
+ def call!(request, env: ENV)
48
+ unless request.is_a?(ProbeRequest)
49
+ raise ArgumentError,
50
+ "request must be an AgentCliRuntime::ProbeRequest"
51
+ end
52
+
53
+ profile = Profiles.resolve(request.profile)
54
+ unless profile.name == :opencode
55
+ raise ArgumentError,
56
+ "route-aware ProbeRequest currently requires profile :opencode"
57
+ end
58
+ installed = profile.binary_installed?(env:, executable: request.executable)
59
+ unless installed
60
+ raise BinaryUnavailable,
61
+ "opencode binary not runnable: " \
62
+ "#{request.executable || profile.bin(env:)}"
63
+ end
64
+
65
+ child_env = child_environment(profile, request, env:)
66
+ version = profile.check_version!(
67
+ env: child_env, executable: request.executable
68
+ )
69
+ evidence = [
70
+ evidence(profile, :installation),
71
+ evidence(profile, :version, [ version ])
72
+ ]
73
+
74
+ run_help = capture!(
75
+ profile, child_env, "run", "--help", executable: request.executable
76
+ )
77
+ missing = REQUIRED_RUN_FLAGS.reject { |flag| advertised?(run_help, flag) }
78
+ unless missing.empty?
79
+ raise UnsupportedCapability,
80
+ "OpenCode run is missing required capability #{missing.join(', ')}"
81
+ end
82
+ evidence.concat(REQUIRED_RUN_FLAGS.map do |flag|
83
+ evidence(profile, capability_for(flag), [ flag ])
84
+ end)
85
+
86
+ export_help = capture!(
87
+ profile, child_env, "export", "--help", executable: request.executable
88
+ )
89
+ unless advertised?(export_help, "--sanitize")
90
+ raise UnsupportedCapability,
91
+ "OpenCode export is missing required --sanitize capability"
92
+ end
93
+ evidence << evidence(profile, :sanitized_export, [ "--sanitize" ])
94
+
95
+ auth_output = capture!(
96
+ profile, child_env, "auth", "list", executable: request.executable
97
+ )
98
+ configured_key = request.credential_environment_keys.find do |key|
99
+ !env[key].to_s.empty?
100
+ end
101
+ auth_from_inventory =
102
+ normalized_output(auth_output).match?(
103
+ /(?:\A|[^A-Za-z0-9_.-])#{Regexp.escape(request.route.provider)}(?:\z|[^A-Za-z0-9_.-])/
104
+ )
105
+ unless configured_key || request.credential_file_staged ||
106
+ auth_from_inventory
107
+ raise AuthenticationError,
108
+ "OpenCode authentication source is missing for requested provider"
109
+ end
110
+ auth = AuthConfiguration.new(
111
+ status: :configured,
112
+ source:
113
+ configured_key ? "selected environment" :
114
+ (request.credential_file_staged ? "staged auth file" :
115
+ "local auth inventory")
116
+ )
117
+ evidence << evidence(profile, :auth_configuration)
118
+
119
+ configured_variants = request.configured_variants
120
+ inventory_variants = if configured_variants.nil?
121
+ models_output = capture!(
122
+ profile, child_env, "models", request.route.provider, "--verbose",
123
+ executable: request.executable,
124
+ timeout_sec: MODEL_INVENTORY_TIMEOUT_SECONDS
125
+ )
126
+ variants_for(models_output, request.route.to_s)
127
+ end
128
+ if configured_variants.nil? && inventory_variants.nil?
129
+ raise RouteUnavailable,
130
+ "requested OpenCode route is unavailable in the local model inventory"
131
+ end
132
+ variants = [ *inventory_variants, *configured_variants ].uniq.sort.freeze
133
+ if request.variant && !variants.include?(request.variant)
134
+ raise RouteUnavailable,
135
+ "requested OpenCode variant is unavailable for the exact route"
136
+ end
137
+ evidence << evidence(profile, :model_route, [ request.route.to_s ])
138
+ if inventory_variants.nil?
139
+ evidence << evidence(
140
+ profile, :configured_model_route, [ request.route.to_s ]
141
+ )
142
+ end
143
+ evidence << evidence(profile, :model_variant, [ request.variant ]) if
144
+ request.variant
145
+
146
+ RouteProbeResult.new(
147
+ provider: profile.name,
148
+ ready: true,
149
+ installed: true,
150
+ executable: request.executable || profile.bin(env: child_env),
151
+ version: version,
152
+ minimum_version: profile.min_version,
153
+ auth_configuration: auth,
154
+ route: request.route,
155
+ route_available: true,
156
+ available_variants: variants,
157
+ capability_evidence: evidence,
158
+ diagnostic: nil
159
+ )
160
+ rescue UnknownProvider
161
+ raise
162
+ rescue Error
163
+ raise
164
+ rescue StandardError => e
165
+ raise ProbeError, Redactor.diagnostic(e)
166
+ end
167
+
168
+ def child_environment(profile, request, env:)
169
+ selected = (ENV.keys | env.keys).to_h { |key| [ key, nil ] }
170
+ env.each do |key, value|
171
+ if SAFE_ENVIRONMENT_KEYS.include?(key) || key.start_with?("LC_") ||
172
+ key.start_with?("MISE_")
173
+ selected[key] = value.to_s
174
+ end
175
+ end
176
+ %w[
177
+ BUNDLE_BIN_PATH BUNDLE_GEMFILE GEM_HOME GEM_PATH RUBYLIB RUBYOPT
178
+ ].each { |key| selected[key] = nil }
179
+ profile.env_bin_override_keys.each do |key|
180
+ selected[key] = env[key].to_s unless env[key].to_s.empty?
181
+ end
182
+ request.credential_environment_keys.each do |key|
183
+ selected[key] = env[key].to_s unless env[key].to_s.empty?
184
+ end
185
+ selected.merge(request.environment).freeze
186
+ end
187
+ private_class_method :child_environment
188
+
189
+ def capture!(profile, environment, *arguments, executable: nil,
190
+ timeout_sec: nil)
191
+ options = { env: environment, executable: executable }
192
+ options[:timeout_sec] = timeout_sec if timeout_sec
193
+ out, err, status = profile.capture_local(*arguments, **options)
194
+ unless status.success?
195
+ diagnostic = normalized_output("#{err}\n#{out}")
196
+ raise ConfigurationError,
197
+ diagnostic.empty? ?
198
+ "OpenCode local inspection command failed" : diagnostic
199
+ end
200
+ "#{out}\n#{err}"
201
+ rescue Errno::ENOENT, Errno::EACCES, Timeout::Error => e
202
+ raise BinaryUnavailable,
203
+ "OpenCode local inspection command could not run (#{e.class.name.split('::').last})"
204
+ end
205
+ private_class_method :capture!
206
+
207
+ def advertised?(help, flag)
208
+ help.match?(/(?:\A|[\s,])#{Regexp.escape(flag)}(?:[\s,=\[]|$)/)
209
+ end
210
+ private_class_method :advertised?
211
+
212
+ def variants_for(output, route)
213
+ text = normalized_output(output)
214
+ lines = text.lines
215
+ index = lines.index { |line| line.strip == route }
216
+ return nil unless index
217
+
218
+ suffix = lines.drop(index + 1).join
219
+ object = first_json_object(suffix)
220
+ return [] unless object
221
+
222
+ parsed = JSON.parse(object)
223
+ variants = parsed["variants"]
224
+ variants.is_a?(Hash) ? variants.keys.sort.freeze : [].freeze
225
+ rescue JSON::ParserError
226
+ raise ConfigurationError,
227
+ "OpenCode local model inventory is malformed"
228
+ end
229
+ private_class_method :variants_for
230
+
231
+ def first_json_object(text)
232
+ start = text.index("{")
233
+ return nil unless start
234
+
235
+ depth = 0
236
+ quoted = false
237
+ escaped = false
238
+ text.each_char.with_index do |character, index|
239
+ next if index < start
240
+
241
+ if quoted
242
+ if escaped
243
+ escaped = false
244
+ elsif character == "\\"
245
+ escaped = true
246
+ elsif character == '"'
247
+ quoted = false
248
+ end
249
+ next
250
+ end
251
+ if character == '"'
252
+ quoted = true
253
+ elsif character == "{"
254
+ depth += 1
255
+ elsif character == "}"
256
+ depth -= 1
257
+ return text[start..index] if depth.zero?
258
+ end
259
+ end
260
+ nil
261
+ end
262
+ private_class_method :first_json_object
263
+
264
+ def normalized_output(value)
265
+ value.to_s.gsub(ANSI_PATTERN, "").strip
266
+ end
267
+ private_class_method :normalized_output
268
+
269
+ def evidence(profile, capability, arguments = [])
270
+ CapabilityEvidence.new(
271
+ capability: capability,
272
+ supported: true,
273
+ provider: profile.name,
274
+ launcher_identity: profile.launcher_identity,
275
+ arguments: arguments.compact
276
+ )
277
+ end
278
+ private_class_method :evidence
279
+
280
+ def capability_for(flag)
281
+ {
282
+ "--model" => :model,
283
+ "--variant" => :model_variant,
284
+ "--format" => :json_events,
285
+ "--dir" => :working_directory,
286
+ "--pure" => :pure,
287
+ "--auto" => :permission_enforcement
288
+ }.fetch(flag)
289
+ end
290
+ private_class_method :capability_for
291
+
292
+ def error_capability(error)
293
+ case error
294
+ when AuthenticationError then :auth_configuration
295
+ when RouteUnavailable then :model_route
296
+ when UnsupportedCapability then :cli_capability
297
+ when BinaryUnavailable then :installation
298
+ else :probe
299
+ end
300
+ end
301
+ private_class_method :error_capability
302
+ end
303
+ end
304
+ end