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

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

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_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_nameObject



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

def binary_name
  "copilot"
end

.discover_modelsObject



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_requirementsObject



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_pathsObject



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_overridesObject



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_nameObject



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

def provider_name
  :github_copilot
end

.smoke_test_contractObject



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_namesObject



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_varsObject



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_typeObject



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

#capabilitiesObject



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_modelsObject



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

def chat_models
  CHAT_MODELS
end

#chat_transportObject



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_typeObject



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

def chat_transport_type
  :openai_compatible
end

#configuration_schemaObject



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_flagsObject



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

def dangerous_mode_flags
  ["--yolo"]
end

#display_nameObject



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

def display_name
  "GitHub Copilot CLI"
end

#error_patternsObject



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_semanticsObject



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

#nameObject



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_varsObject



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_transportsObject



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