Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion autolith.asd
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
(:file "configuration/preferences")
(:file "configuration/permissions")
(:file "provider/authentication")
(:file "provider/chatgpt/authentication")
(:file "provider/gemini/authentication")
(:file "provider/grok/authentication")
(:file "provider/api-key")
Expand Down Expand Up @@ -201,6 +202,7 @@
:components ((:module "tests"
:serial t
:components ((:file "test-support")
(:file "device-authentication-test-support")
(:file "stream-tests")
(:file "memory-tests")
(:file "papercut-tests")
Expand All @@ -223,6 +225,7 @@
(:file "conversation-replay-tests")
(:file "plan-tests")
(:file "authentication-tests")
(:file "chatgpt-authentication-tests")
(:file "gemini-authentication-tests")
(:file "grok-authentication-tests")
(:file "nous-authentication-tests")
Expand All @@ -249,7 +252,7 @@
(:file "recovery-tests")
(:file "lisp-worker-tests")
(:file "self-tool-tests")
(:file "device-authentication-tests")
(:file "device-authentication-tests")
(:file "nous-device-authentication-tests")
(:file "agent-tests")
(:file "inference-tests")
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.org
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ provider request --> semantic events --> agent --> validated tool calls
- Configuration records credential *locations* only
- A provider request sees tokens only in its dynamic scope
- The optional Codex file is a one-time access-token import
- Renewable credentials come from Autolith's own device flow
- Renewable ChatGPT credentials come from browser OAuth or the optional device-code flow

* Workspace and workers

Expand Down
13 changes: 9 additions & 4 deletions docs/guide.org
Original file line number Diff line number Diff line change
Expand Up @@ -433,10 +433,15 @@ available. =(models)= triggers discovery. You can also pass non-secret
=:headers= and static =:models= for descriptions, windows, efforts, or
providers that have no model-list endpoint.

Built-in ChatGPT, Grok, and Nous Research subscriptions authenticate through a
browser device flow. Gemini uses Google installed-application OAuth with a local
loopback callback. Anthropic, Fireworks, OpenCode, OpenRouter, Mistral, and
user-registered OpenAI-compatible providers use API keys.
Built-in ChatGPT subscriptions use browser OAuth with PKCE and a local callback
on =localhost:1455= or =localhost:1457= by default. Use
=/auth chatgpt device=, =(auth "chatgpt" "device")=, or
=autolith auth chatgpt device= to select the device-code flow instead. The
browser authorization URL is always printed for manual use. Grok and Nous
Research use browser device flows. Gemini uses Google installed-application
OAuth with a local loopback callback.
Anthropic, Fireworks, OpenCode, OpenRouter, Mistral, and user-registered
OpenAI-compatible providers use API keys.

ChatGPT Codex Fast mode sends =service_tier=priority= when the active Codex model
advertises Fast support and uses 2x plan usage. The built-in GPT-5.6 models
Expand Down
25 changes: 14 additions & 11 deletions src/application/commands.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -1710,9 +1710,11 @@ are forwarded to TERMINAL-UI-SELECT."
(terminal-authentication-streams
(terminal-ui-terminal (application-ui application))))

(-> application-authenticate (application string) null)
(defun application-authenticate (application provider-name)
"Authenticate APPLICATION's explicitly named provider."
(-> application-authenticate
(application string &optional (or null string symbol))
null)
(defun application-authenticate (application provider-name &optional method)
"Authenticate APPLICATION's explicitly named provider using optional METHOD."
(let* ((ui (application-ui application))
(provider (application--authentication-provider application provider-name))
(message nil))
Expand All @@ -1732,10 +1734,11 @@ are forwarded to TERMINAL-UI-SELECT."
(*api-key-input-file-descriptor* input-file-descriptor)
(*api-key-output-styled-p*
(terminal-styled-p (terminal-ui-terminal ui))))
(setf message
(provider-authenticate provider
:stream output
:open-browser-p t)))
(setf message
(provider-authenticate-with-method
provider method
:stream output
:open-browser-p t)))
(if stop-ui-p
(terminal-ui-start ui)
(application--authentication-ui-resume ui))))
Expand Down Expand Up @@ -2065,20 +2068,20 @@ are forwarded to TERMINAL-UI-SELECT."

(define-application-command application--builtin-authentication-command
(:name "/auth"
:argument "[PROVIDER]"
:argument "[PROVIDER] [METHOD]"
:description "pick and authenticate a registered provider"
:tip "starts direct authentication for an explicitly selected provider."
:tip "uses browser auth by default; ChatGPT also accepts device."
:busy-behavior :hold
:terminal-behavior :exclusive
:callable t)
(application &optional (provider-name nil provider-name-supplied-p))
(application &optional (provider-name nil provider-name-supplied-p) method)
(let ((provider-name
(if provider-name-supplied-p
provider-name
(and *application-command-interactive-p*
(application--pick-authentication-provider application)))))
(when provider-name
(application-authenticate application provider-name)))
(application-authenticate application provider-name method)))
':continue)

(define-application-command application--builtin-model-command
Expand Down
30 changes: 30 additions & 0 deletions src/configuration/settings.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,43 @@
"https://chatgpt.com/backend-api/codex/responses"
"The current ChatGPT Codex Responses endpoint.")

;; ChatGPT browser OAuth behavior inspected at
;; https://github.com/openai/codex commit
;; 94cbbddafc1776d5e377bca1b05932c697e82238.
(defparameter *openai-oauth-issuer* "https://auth.openai.com"
"The OpenAI issuer serving ChatGPT browser OAuth.")

(defparameter *openai-oauth-token-endpoint*
"https://auth.openai.com/oauth/token"
"The OpenAI OAuth token endpoint.")

(defparameter *openai-oauth-client-id* "app_EMoamEEZ73f0CkXaXp7hrann"
"The public OAuth client identifier used by Codex-compatible clients.")

(defparameter *openai-oauth-scopes*
'("openid"
"profile"
"email"
"offline_access"
"api.connectors.read"
"api.connectors.invoke")
"The scopes requested by ChatGPT browser OAuth.")

(defparameter *openai-oauth-originator* "autolith"
"The honest client originator sent during ChatGPT browser OAuth.")

(defparameter *chatgpt-oauth-callback-ports* '(1455 1457)
"The localhost callback ports allowed by the ChatGPT OAuth client.")

(defparameter *chatgpt-oauth-callback-timeout* 900
"The maximum seconds to wait for the ChatGPT browser callback.")

(defparameter *chatgpt-oauth-request-timeout* 5
"The maximum seconds allowed to read one local callback request line.")

(defparameter *chatgpt-oauth-request-line-limit* 8192
"The maximum characters accepted in one local callback request line.")

;; Gemini CLI OAuth behavior inspected at google-gemini/gemini-cli commit
;; 0bd1d439751478771c45d3d0895a6a9760554bf4. The installed application uses
;; PKCE as a public client. Autolith deliberately does not embed its client
Expand Down
53 changes: 53 additions & 0 deletions src/provider/authentication.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,59 @@ its protocol-level close operation."
(json-object-p (aref organizations 0)))
(json-get (aref organizations 0) "id")))))))


;;;; -- OAuth Wire Helpers --

(-> authentication-user-agent () string)
(defun authentication-user-agent ()
"Return the honest Autolith user agent sent to authentication services."
(format nil "autolith/~A (~A ~A; ~A)"
*autolith-version*
(software-type)
(software-version)
(machine-type)))

(-> oauth--base64url ((simple-array (unsigned-byte 8) (*))) string)
(defun oauth--base64url (octets)
"Return OCTETS as unpadded RFC 4648 Base64url text."
(string-right-trim
'(#\=)
(substitute #\_
#\/
(substitute #\-
#\+
(usb8-array-to-base64-string octets)))))

(-> oauth--create-pkce (&key (:verifier-octets integer)) (values string string))
(defun oauth--create-pkce (&key (verifier-octets 32))
"Return a fresh PKCE verifier and S256 challenge from VERIFIER-OCTETS."
(unless (plusp verifier-octets)
(error 'authentication-error
:message "The OAuth PKCE verifier size must be positive."))
(let* ((verifier (oauth--base64url (random-data verifier-octets)))
(octets (map '(simple-array (unsigned-byte 8) (*)) #'char-code verifier))
(challenge
(oauth--base64url
(ironclad:digest-sequence ':sha256 octets))))
(values verifier challenge)))

(-> oauth--query-parameters (string) list)
(defun oauth--query-parameters (target)
"Decode TARGET's query string into an association list."
(let ((question (position #\? target)))
(when question
(loop with query = (subseq target (1+ question))
with start = 0
for end = (position #\& query :start start)
for field = (subseq query start end)
for equals = (position #\= field)
collect (cons (url-decode (subseq field 0 equals))
(url-decode (if equals
(subseq field (1+ equals))
"")))
while end
do (setf start (1+ end))))))

;;;; -- Credential Sources --

(defclass credential-source (cl-rfc8628:credential-source)
Expand Down
Loading