Class: AgentHarness::Providers::GithubCopilot

Inherits:
Base
  • Object
show all
Includes:
McpConfigFileSupport, TokenUsageParsing
Defined in:
lib/agent_harness/providers/github_copilot.rb

Constant Summary collapse

CLI_PACKAGE =
"@github/copilot"
INSTALL_COMMAND_PREFIX =
["npm", "install", "-g"].freeze
DEFAULT_MAX_AUTOPILOT_CONTINUES =
50
LEGACY_BINARY_NAME =
"github-copilot-cli"
MODEL_PATTERN =
/^gpt-[\d.o-]+(?:-turbo)?(?:-mini)?$/i
GITHUB_MODELS_BASE_URL =
"https://models.inference.ai.azure.com"
CHAT_DEFAULT_MODEL =
"gpt-4o"
CHAT_MODELS =
%w[gpt-4o gpt-4o-mini gpt-4-turbo].freeze
SMOKE_TEST_CONTRACT =
{
  prompt: "Reply with exactly OK.",
  expected_output: "OK",
  timeout: 30,
  require_output: true,
  success_message: "Smoke test passed"
}.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 included from McpConfigFileSupport

#cleanup_mcp_tempfiles!, #write_mcp_config_file

Methods inherited from Base

#check_quota, #cli_env_overrides, #configure, #initialize, #parse_test_error, #plan_execution, #preflight_check, #sandboxed_environment?, #send_chat_message, #test_command_overrides, #update_quota_from_headers

Methods included from RateLimitResetParsing

#parse_rate_limit_reset

Methods included from Adapter

#auth_lock_config, #config_file_content, #error_classification_patterns, #fetch_mcp_servers, #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, #plan_execution, #preflight_check, #session_flags, #smoke_test, #smoke_test_contract, #supports_activity_heartbeat?, #supports_dangerous_mode?, #supports_message_tool_injection?, #supports_text_mode?, #supports_tool_control?, #token_usage_from_api_response, #validate_config, #validate_mcp_servers!

Constructor Details

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

Class Method Details

.available? ⇒ Boolean

Returns:

  • (Boolean)


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

def available?
  executor = AgentHarness.configuration.command_executor
  return false unless executor.which(binary_name)

  true
rescue
  false
end

.binary_name ⇒ Object



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

def binary_name
  "copilot"
end

.discover_models ⇒ Object



110
111
112
113
114
115
116
117
118
# File 'lib/agent_harness/providers/github_copilot.rb', line 110

def discover_models
  return [] unless available?

  [
    {name: "gpt-4o", family: "gpt-4o", tier: "standard", provider: "github_copilot"},
    {name: "gpt-4o-mini", family: "gpt-4o-mini", tier: "mini", provider: "github_copilot"},
    {name: "gpt-4-turbo", family: "gpt-4-turbo", tier: "advanced", provider: "github_copilot"}
  ]
end

.firewall_requirements ⇒ Object



87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/agent_harness/providers/github_copilot.rb', line 87

def firewall_requirements
  {
    domains: [
      "copilot-proxy.githubusercontent.com",
      "api.githubcopilot.com",
      "copilot-telemetry.githubusercontent.com",
      "default.exp-tas.com",
      "copilot-completions.githubusercontent.com"
    ],
    ip_ranges: []
  }
end

.install_command(version: nil) ⇒ Object



71
72
73
# File 'lib/agent_harness/providers/github_copilot.rb', line 71

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

.installation_contract(version: nil) ⇒ Object



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/agent_harness/providers/github_copilot.rb', line 50

def installation_contract(version: nil)
  normalized_version = normalize_install_version(version)
  package = normalized_version ? "#{CLI_PACKAGE}@#{normalized_version}" : CLI_PACKAGE
  install_command = (INSTALL_COMMAND_PREFIX + [package]).freeze

  contract = {
    source: :npm,
    package: package,
    package_name: CLI_PACKAGE,
    version: normalized_version,
    binary_name: binary_name,
    install_command_prefix: INSTALL_COMMAND_PREFIX,
    install_command: install_command
  }

  contract.each_value do |value|
    value.freeze if value.is_a?(String)
  end
  contract.freeze
end

.instruction_file_paths ⇒ Object



100
101
102
103
104
105
106
107
108
# File 'lib/agent_harness/providers/github_copilot.rb', line 100

def instruction_file_paths
  [
    {
      path: ".github/copilot-instructions.md",
      description: "GitHub Copilot agent instructions",
      symlink: true
    }
  ]
end

.model_family(provider_model_name) ⇒ Object



128
129
130
# File 'lib/agent_harness/providers/github_copilot.rb', line 128

def model_family(provider_model_name)
  provider_model_name
end

.provider_metadata_overrides ⇒ Object



75
76
77
78
79
80
81
82
83
84
85
# File 'lib/agent_harness/providers/github_copilot.rb', line 75

def 
  {
    auth: {
      service: :github,
      api_family: :github_copilot
    },
    identity: {
      bot_usernames: ["github-copilot[bot]"]
    }
  }
end

.provider_model_name(family_name) ⇒ Object



132
133
134
# File 'lib/agent_harness/providers/github_copilot.rb', line 132

def provider_model_name(family_name)
  family_name
end

.provider_name ⇒ Object



33
34
35
# File 'lib/agent_harness/providers/github_copilot.rb', line 33

def provider_name
  :github_copilot
end

.smoke_test_contract ⇒ Object



124
125
126
# File 'lib/agent_harness/providers/github_copilot.rb', line 124

def smoke_test_contract
  SMOKE_TEST_CONTRACT
end

.supports_chat? ⇒ Boolean

Returns:

  • (Boolean)


120
121
122
# File 'lib/agent_harness/providers/github_copilot.rb', line 120

def supports_chat?
  true
end

.supports_model_family?(family_name) ⇒ Boolean

Returns:

  • (Boolean)


136
137
138
# File 'lib/agent_harness/providers/github_copilot.rb', line 136

def supports_model_family?(family_name)
  MODEL_PATTERN.match?(family_name)
end

Instance Method Details

#api_key_env_var_names ⇒ Object



220
221
222
# File 'lib/agent_harness/providers/github_copilot.rb', line 220

def api_key_env_var_names
  ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"]
end

#api_key_unset_vars ⇒ Object



224
225
226
# File 'lib/agent_harness/providers/github_copilot.rb', line 224

def api_key_unset_vars
  ["COPILOT_PROVIDER_API_KEY", "COPILOT_PROVIDER_BASE_URL"]
end

#auth_type ⇒ Object



232
233
234
# File 'lib/agent_harness/providers/github_copilot.rb', line 232

def auth_type
  :oauth
end

#build_command(prompt, options) ⇒ Object



320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# File 'lib/agent_harness/providers/github_copilot.rb', line 320

def build_command(prompt, options)
  runtime = options[:provider_runtime]
  cmd = [
    self.class.binary_name,
    "--autopilot",
    "--max-autopilot-continues",
    max_autopilot_continues(options).to_s,
    "--output-format",
    "json"
  ]
  # Smoke tests must run non-interactively; force full-permission mode
  # so autopilot does not stall on permission prompts.
  cmd += dangerous_mode_flags if (options[:dangerous_mode] || options[:smoke_test]) && supports_dangerous_mode?

  if options[:mcp_servers]&.any?
    cmd += build_mcp_flags(options[:mcp_servers], options: options)
  end

  cmd += @config.default_flags if @config.default_flags&.any?

  model = effective_model_name(runtime)
  cmd += ["--model", model] if model

  if runtime
    runtime_flags = runtime.flags
    cmd += runtime_flags unless runtime_flags.empty?
  end

  cmd += test_command_overrides if options[:smoke_test]
  cmd += ["-p", prompt]
  cmd
end

#build_env(options) ⇒ Object



353
354
355
356
357
358
359
# File 'lib/agent_harness/providers/github_copilot.rb', line 353

def build_env(options)
  env = super
  needs_full_permissions = options[:dangerous_mode] || options[:smoke_test]
  return env unless needs_full_permissions && supports_dangerous_mode?

  env.merge("COPILOT_ALLOW_ALL" => "true")
end

#build_execution_preparation(options) ⇒ Object



361
362
363
364
365
366
367
368
369
370
371
372
373
374
# File 'lib/agent_harness/providers/github_copilot.rb', line 361

def build_execution_preparation(options)
  return nil unless options[:mcp_servers]&.any?

  plan = mcp_config_plan(options, options[:mcp_servers])
  ExecutionPreparation.new(
    file_writes: [
      {
        path: plan.fetch(:path),
        content: plan.fetch(:content),
        mode: 0o600
      }
    ]
  )
end

#build_mcp_flags(mcp_servers, options:) ⇒ Object



248
249
250
251
252
# File 'lib/agent_harness/providers/github_copilot.rb', line 248

def build_mcp_flags(mcp_servers, options:)
  return [] if mcp_servers.empty?

  ["--additional-mcp-config", "@#{mcp_config_plan(options, mcp_servers).fetch(:path)}"]
end

#build_runtime_chat_transport(runtime) ⇒ Object



207
208
209
210
211
212
213
214
# File 'lib/agent_harness/providers/github_copilot.rb', line 207

def build_runtime_chat_transport(runtime)
  OpenAICompatibleTransport.new(
    base_url: runtime.chat_base_url || GITHUB_MODELS_BASE_URL,
    api_key: runtime.chat_api_key || resolve_chat_api_key,
    model: runtime.chat_model || runtime.model || CHAT_DEFAULT_MODEL,
    logger: @logger
  )
end

#capabilities ⇒ Object



178
179
180
181
182
183
184
185
186
187
188
# File 'lib/agent_harness/providers/github_copilot.rb', line 178

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

#chat_models ⇒ Object



194
195
196
# File 'lib/agent_harness/providers/github_copilot.rb', line 194

def chat_models
  CHAT_MODELS
end

#chat_transport ⇒ Object



198
199
200
201
202
203
204
205
# File 'lib/agent_harness/providers/github_copilot.rb', line 198

def chat_transport
  @chat_transport ||= OpenAICompatibleTransport.new(
    base_url: GITHUB_MODELS_BASE_URL,
    api_key: resolve_chat_api_key,
    model: CHAT_DEFAULT_MODEL,
    logger: @logger
  )
end

#chat_transport_type ⇒ Object



216
217
218
# File 'lib/agent_harness/providers/github_copilot.rb', line 216

def chat_transport_type
  :openai_compatible
end

#configuration_schema ⇒ Object



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/agent_harness/providers/github_copilot.rb', line 161

def configuration_schema
  {
    fields: [
      {
        name: :model,
        type: :string,
        label: "Model",
        required: false,
        hint: "Copilot model identifier (for example gpt-4o or gpt-4o-mini)",
        accepts_arbitrary: true
      }
    ],
    auth_modes: [:oauth],
    openai_compatible: false
  }
end

#dangerous_mode_flags ⇒ Object



236
237
238
# File 'lib/agent_harness/providers/github_copilot.rb', line 236

def dangerous_mode_flags
  ["--yolo"]
end

#display_name ⇒ Object



157
158
159
# File 'lib/agent_harness/providers/github_copilot.rb', line 157

def display_name
  "GitHub Copilot CLI"
end

#error_patterns ⇒ Object



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/agent_harness/providers/github_copilot.rb', line 271

def error_patterns
  {
    auth_expired: [
      /not.?logged.?in/i,
      /not.?authorized/i,
      /authentication/i,
      /token.*invalid/i,
      /copilot requests/i
    ],
    rate_limited: [
      /rate.?limit/i,
      /too.?many.?requests/i,
      /\b429\b/
    ],
    transient: [
      /connection.?error/i,
      /timeout/i,
      /try.?again/i,
      /\b502\b/,
      /\b503\b/
    ],
    permanent: [
      /unknown.?flag/i,
      /invalid.?value/i,
      /continuation limit/i,
      /max.?autopilot.?continues/i
    ]
  }
end

#execution_semantics ⇒ Object



258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/agent_harness/providers/github_copilot.rb', line 258

def execution_semantics
  {
    prompt_delivery: :arg,
    output_format: :json,
    sandbox_aware: false,
    uses_subcommand: false,
    non_interactive_flag: "--autopilot",
    legitimate_exit_codes: [0],
    stderr_is_diagnostic: true,
    parses_rate_limit_reset: false
  }
end

#name ⇒ Object



153
154
155
# File 'lib/agent_harness/providers/github_copilot.rb', line 153

def name
  "github_copilot"
end

#parse_container_output(stdout:, stderr: "", exit_code: 0, duration: 0.0, **_options) ⇒ Object



376
377
378
379
380
381
382
383
384
# File 'lib/agent_harness/providers/github_copilot.rb', line 376

def parse_container_output(stdout:, stderr: "", exit_code: 0, duration: 0.0, **_options)
  result = CommandExecutor::Result.new(
    stdout: stdout,
    stderr: stderr,
    exit_code: exit_code,
    duration: duration
  )
  parse_response(result, duration: duration)
end

#send_message(prompt:, **options) ⇒ Object



314
315
316
317
318
# File 'lib/agent_harness/providers/github_copilot.rb', line 314

def send_message(prompt:, **options)
  super
ensure
  cleanup_mcp_tempfiles!
end

#subscription_unset_vars ⇒ Object



228
229
230
# File 'lib/agent_harness/providers/github_copilot.rb', line 228

def subscription_unset_vars
  api_key_env_var_names + api_key_unset_vars
end

#supported_mcp_transports ⇒ Object



244
245
246
# File 'lib/agent_harness/providers/github_copilot.rb', line 244

def supported_mcp_transports
  %w[stdio http sse]
end

#supports_chat? ⇒ Boolean

Returns:

  • (Boolean)


190
191
192
# File 'lib/agent_harness/providers/github_copilot.rb', line 190

def supports_chat?
  true
end

#supports_mcp? ⇒ Boolean

Returns:

  • (Boolean)


240
241
242
# File 'lib/agent_harness/providers/github_copilot.rb', line 240

def supports_mcp?
  true
end

#supports_sessions? ⇒ Boolean

Returns:

  • (Boolean)


254
255
256
# File 'lib/agent_harness/providers/github_copilot.rb', line 254

def supports_sessions?
  false
end

#supports_token_counting? ⇒ Boolean

Returns:

  • (Boolean)


310
311
312
# File 'lib/agent_harness/providers/github_copilot.rb', line 310

def supports_token_counting?
  true
end

#translate_error(message) ⇒ Object



301
302
303
304
305
306
307
308
# File 'lib/agent_harness/providers/github_copilot.rb', line 301

def translate_error(message)
  case message
  when /copilot.*not found/i, /No such file or directory - copilot/i
    "GitHub Copilot CLI not installed."
  else
    message
  end
end