iachat/scripts/captain_codex_poc/login.rb
Rodribm10 df56ee8115 chore(captain): PoC Codex OAuth device flow + Responses streaming
PoC validado com conta ChatGPT Plus e client_id do Hermes. Device flow
OAuth funciona, gera access_token + refresh_token auto-refresh. Chat e
function calling funcionaram em gpt-5.4, gpt-5.4-mini, gpt-5.2 e
gpt-5.3-codex.

Descobertas pro adapter final:
- Endpoint: /responses (não /chat/completions)
- Streaming obrigatório (stream: true)
- store: false obrigatório
- Sem temperature/top_p (modelos reasoning)
- input[] no lugar de messages[]
- instructions top-level no lugar de system role
- Tools sem wrapping function: {}
- Output via events response.output_item.done (não response.completed)

Pasta scripts/captain_codex_poc/ está excluída do Rubocop (scripts
standalone, não rodam em contexto Rails).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 14:56:57 -03:00

115 lines
3.2 KiB
Ruby
Executable File

#!/usr/bin/env ruby
# Device code flow OAuth com OpenAI — reutiliza client_id do Hermes.
# Salva tokens em scripts/captain_codex_poc/tokens.json.
require 'net/http'
require 'uri'
require 'json'
require 'time'
CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann'.freeze
ISSUER = 'https://auth.openai.com'.freeze
TOKENS_PATH = File.expand_path('tokens.json', __dir__).freeze
def http_json(method, url, body: nil, headers: {}, form: false)
uri = URI(url)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.read_timeout = 30
req_class = method == :get ? Net::HTTP::Get : Net::HTTP::Post
req = req_class.new(uri)
headers.each { |k, v| req[k] = v }
if body
if form
req['Content-Type'] = 'application/x-www-form-urlencoded'
req.body = URI.encode_www_form(body)
else
req['Content-Type'] = 'application/json'
req.body = JSON.generate(body)
end
end
resp = http.request(req)
[resp.code.to_i, (resp.body.nil? || resp.body.empty? ? {} : (JSON.parse(resp.body) rescue { 'raw' => resp.body }))]
end
puts '=== Captain Codex OAuth — Device Flow ==='
puts
# Passo 1: Pedir device code
puts '[1/4] Solicitando device code...'
status, data = http_json(:post, "#{ISSUER}/api/accounts/deviceauth/usercode", body: { client_id: CLIENT_ID })
abort "Falhou no device code (HTTP #{status}): #{data}" unless status == 200
user_code = data.fetch('user_code')
device_auth_id = data.fetch('device_auth_id')
poll_interval = [3, (data['interval'] || 5).to_i].max
puts
puts '[2/4] Abra o browser na URL abaixo e cole o código:'
puts
puts " URL: \e[36m#{ISSUER}/codex/device\e[0m"
puts " Code: \e[93m#{user_code}\e[0m"
puts
puts "Aguardando autorização (timeout 15min, Ctrl+C para cancelar)..."
# Passo 2: Polling
auth_code = nil
verifier = nil
deadline = Time.now + (15 * 60)
loop do
abort "\nTimeout de 15min atingido." if Time.now > deadline
sleep poll_interval
code, resp = http_json(:post, "#{ISSUER}/api/accounts/deviceauth/token", body: {
device_auth_id: device_auth_id,
user_code: user_code
})
case code
when 200
auth_code = resp.fetch('authorization_code')
verifier = resp.fetch('code_verifier')
break
when 403, 404
print '.'
next
else
abort "\nErro no polling (HTTP #{code}): #{resp}"
end
end
puts "\n[3/4] Autorização recebida, trocando por tokens..."
# Passo 3: Exchange code -> tokens
redirect_uri = "#{ISSUER}/deviceauth/callback"
status, tokens = http_json(:post, "#{ISSUER}/oauth/token", body: {
grant_type: 'authorization_code',
code: auth_code,
redirect_uri: redirect_uri,
client_id: CLIENT_ID,
code_verifier: verifier
}, form: true)
abort "Token exchange falhou (HTTP #{status}): #{tokens}" unless status == 200
access_token = tokens.fetch('access_token')
refresh_token = tokens.fetch('refresh_token')
expires_in = Integer(tokens['expires_in'] || 3600)
expires_at = Time.now + expires_in
File.write(TOKENS_PATH, JSON.pretty_generate({
access_token: access_token,
refresh_token: refresh_token,
expires_at: expires_at.iso8601
}))
puts
puts "[4/4] Sucesso!"
puts " Tokens salvos em: #{TOKENS_PATH}"
puts " Access token expira em: #{expires_at}"
puts
puts "Próximo passo: ruby scripts/captain_codex_poc/test_chat.rb"