diff options
| author | t <t@tjp.lol> | 2026-10-16 20:13:59 -0600 |
|---|---|---|
| committer | t <t@tjp.lol> | 2026-10-16 20:42:56 -0600 |
| commit | 3fd3bd6b58b1c9404075c8256ff3ae99fc5b542c (patch) | |
| tree | c9c0a095e320c6487a65ce5d3529890dd05ec5c9 | |
initial commit; basically working
| -rw-r--r-- | LICENSE | 18 | ||||
| -rw-r--r-- | README.md | 61 | ||||
| -rw-r--r-- | _selfcheck.lua | 113 | ||||
| -rw-r--r-- | init.lua | 72 | ||||
| -rw-r--r-- | panto-codex-scm-1.rockspec | 19 |
5 files changed, 283 insertions, 0 deletions
@@ -0,0 +1,18 @@ +MIT License + +Copyright (c) 2026 Travis Parker + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b851b4d --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# panto-codex + +Codex subscription defaults for panto. Mark named tables; the extension fills the boilerplate. + +## Setup + +Requires panto's `configure(config)` extension hook (before auth/provider validation). + +```toml +[extensions] +rocks = ["panto-codex"] + +[auth.codex] +panto_codex_extension = true + +[providers.codex] +auth = "codex" +panto_codex_extension = true +``` + +```sh +panto auth login codex +panto models sync +panto models list codex: +``` + +## Defaults + +| Setting | Value | +| --- | --- | +| Auth | Codex device OAuth; credentials and refresh stay in panto | +| Provider | Responses API with the Codex dialect; `openai` model catalog | +| Endpoint | `https://chatgpt.com/backend-api/codex` | +| Headers | `OpenAI-Beta: responses=experimental`, `originator: codex_cli_rs` | + +## Rules + +- Only `panto_codex_extension = true` opts in; auth/provider names are arbitrary. +- Keep `providers.<name>.auth` explicit; the extension never guesses linkage. +- Explicit values win, including nested endpoints and case-insensitive headers. +- Defaults are independent per table/load; no config files are rewritten. +- No network or credentials in Lua; env/session placeholders stay raw for panto. +- Rigs must include this extension themselves; `--no-extensions` skips it. + +Flat and endpoint-table provider configurations are supported. Overrides must +remain valid panto configuration. + +## Packaging / checks + +```sh +panto lua _selfcheck.lua +luarocks lint panto-codex-scm-1.rockspec +``` + +The rockspec maps `require("panto-codex")` to `init.lua`; no dependencies +beyond Lua. + +## References + +- [OpenAI device-login implementation](https://github.com/openai/codex/blob/main/codex-rs/login/src/device_code_auth.rs) +- [OpenAI authentication documentation](https://developers.openai.com/codex/auth/) diff --git a/_selfcheck.lua b/_selfcheck.lua new file mode 100644 index 0000000..f3be748 --- /dev/null +++ b/_selfcheck.lua @@ -0,0 +1,113 @@ +-- Offline: panto lua _selfcheck.lua (or Lua 5.4). +local here = debug.getinfo(1, "S").source:match("^@(.*)/[^/]+$") or "." +local entry = assert(loadfile(here .. "/init.lua"))() +assert(entry.name == "codex" and type(entry.configure) == "function") +assert(entry.activate == nil, "configure-only extension") + +local function contains(actual, expected) + for key, value in pairs(expected) do + if type(value) == "table" then + assert(type(actual[key]) == "table", key .. " must be a table") + contains(actual[key], value) + else + assert(actual[key] == value, key .. " default mismatch") + end + end +end + +local empty = {} +entry.configure(empty) +assert(next(empty) == nil, "no implicit auth/providers") + +local untouched = { providers = { + absent = {}, disabled = { panto_codex_extension = false }, + string_marker = { panto_codex_extension = "true" }, +}, auth = { absent = {}, disabled = { panto_codex_extension = false } } } +entry.configure(untouched) +assert(next(untouched.providers.absent) == nil) +assert(untouched.providers.disabled.style == nil and untouched.providers.string_marker.style == nil) +assert(next(untouched.auth.absent) == nil and untouched.auth.disabled.type == nil) + +local config = { + auth = { first = { panto_codex_extension = true }, second = { panto_codex_extension = true } }, + providers = { + first = { panto_codex_extension = true, auth = "second" }, + second = { panto_codex_extension = true, auth = "first" }, + }, +} +entry.configure(config) +contains(config.auth.first, { + type = "oauth_device", + dialect = "codex", + client_id = "app_EMoamEEZ73f0CkXaXp7hrann", + device_code_url = "https://auth.openai.com/api/accounts/deviceauth/usercode", + device_poll_url = "https://auth.openai.com/api/accounts/deviceauth/token", + verification_url = "https://auth.openai.com/codex/device", + token_url = "https://auth.openai.com/oauth/token", + account_id_jwt_claim = "https://api.openai.com/auth", +}) +contains(config.providers.first, { + style = "openai_responses", + model_catalog_name = "openai", + extra_headers = { + ["OpenAI-Beta"] = "responses=experimental", + originator = "codex_cli_rs", + }, + base_url = "https://chatgpt.com/backend-api/codex", + dialect = "codex", +}) +assert(config.providers.first.auth == "second" and config.providers.second.auth == "first") +assert(config.providers.first.extra_headers ~= config.providers.second.extra_headers) +config.providers.first.extra_headers.custom = "first-only" +assert(config.providers.second.extra_headers.custom == nil, "instance defaults must not alias") +entry.configure(config) +assert(config.providers.first.extra_headers.custom == "first-only", "repeatable configuration") + +local fresh = { providers = { third = { panto_codex_extension = true } } } +entry.configure(fresh) +assert(fresh.providers.third.auth == nil, "never invent auth linkage") +assert(fresh.providers.third.extra_headers.custom == nil, "no cross-load mutable defaults") + +local override = { + auth = { login = { panto_codex_extension = true, client_id = "custom-client", key = "${env:CUSTOM_KEY}" } }, + providers = { custom = { + panto_codex_extension = true, + auth = "login", + model_catalog_name = "custom-catalog", + extra_headers = { ["openai-beta"] = "custom-header", custom = "${session:id}" }, + endpoints = { openai_responses = { + base_url = "https://gateway.example/v1", + extra_headers = { local_header = "kept" }, + } }, + } }, +} +entry.configure(override) +local custom = override.providers.custom +assert(override.auth.login.client_id == "custom-client") +assert(override.auth.login.key == "${env:CUSTOM_KEY}", "raw key reference preserved") +assert(custom.auth == "login" and custom.model_catalog_name == "custom-catalog") +assert(custom.base_url == nil and custom.dialect == nil, "endpoint form stays valid") +assert(custom.endpoints.openai_responses.base_url == "https://gateway.example/v1") +assert(custom.endpoints.openai_responses.extra_headers.local_header == "kept") +assert(custom.extra_headers["openai-beta"] == "custom-header") +assert(custom.extra_headers["OpenAI-Beta"] == nil, "case-insensitive header override") +assert(custom.extra_headers.custom == "${session:id}", "raw session reference preserved") + +assert(custom.endpoints.openai_responses.dialect == "codex") + +for _, style in ipairs({ "openai_chat", "anthropic_messages" }) do + local flat = { providers = { custom = { + panto_codex_extension = true, auth = "login", style = style, + base_url = "https://gateway.example/v1", + } } } + entry.configure(flat) + assert(flat.providers.custom.endpoints == nil, "explicit flat provider stays flat") + assert(flat.providers.custom.style == style and flat.providers.custom.dialect == nil) + assert(flat.providers.custom.base_url == "https://gateway.example/v1") +end + +local invalid = { providers = { marked = { panto_codex_extension = true, extra_headers = false } } } +entry.configure(invalid) +assert(invalid.providers.marked.extra_headers == false, "explicit false remains untouched") + +print("panto-codex: ok") diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..32c98de --- /dev/null +++ b/init.lua @@ -0,0 +1,72 @@ +-- Codex subscription defaults for panto. No login, networking, or session activation. +local auth_defaults = { + type = "oauth_device", + dialect = "codex", + client_id = "app_EMoamEEZ73f0CkXaXp7hrann", + device_code_url = "https://auth.openai.com/api/accounts/deviceauth/usercode", + device_poll_url = "https://auth.openai.com/api/accounts/deviceauth/token", + verification_url = "https://auth.openai.com/codex/device", + token_url = "https://auth.openai.com/oauth/token", + account_id_jwt_claim = "https://api.openai.com/auth", +} + +local provider_defaults = { + style = "openai_responses", + model_catalog_name = "openai", + extra_headers = { + ["OpenAI-Beta"] = "responses=experimental", + originator = "codex_cli_rs", + }, +} + +local endpoint_defaults = { + openai_responses = { + base_url = "https://chatgpt.com/backend-api/codex", + dialect = "codex", + }, +} + +-- Fill only missing values; HTTP header names are case-insensitive. +local function fill_missing(target, defaults, headers) + for key, value in pairs(defaults) do + local target_key = key + if headers then + for existing in pairs(target) do + if type(existing) == "string" and existing:lower() == key:lower() then + target_key = existing + break + end + end + end + if target[target_key] == nil then + target[target_key] = type(value) == "table" and {} or value + end + if type(value) == "table" and type(target[target_key]) == "table" then + fill_missing(target[target_key], value, key == "extra_headers") + end + end +end + +return { + name = "codex", + configure = function(config) + for _, auth in pairs(config.auth or {}) do + if type(auth) == "table" and auth.panto_codex_extension == true then + fill_missing(auth, auth_defaults) + end + end + for _, provider in pairs(config.providers or {}) do + if type(provider) == "table" and provider.panto_codex_extension == true then + fill_missing(provider, provider_defaults) + if provider.endpoints == nil then + fill_missing(provider, { base_url = endpoint_defaults.openai_responses.base_url }) + if provider.style == "openai_responses" then + fill_missing(provider, { dialect = endpoint_defaults.openai_responses.dialect }) + end + else + fill_missing(provider, { endpoints = endpoint_defaults }) + end + end + end + end, +} diff --git a/panto-codex-scm-1.rockspec b/panto-codex-scm-1.rockspec new file mode 100644 index 0000000..756ac4f --- /dev/null +++ b/panto-codex-scm-1.rockspec @@ -0,0 +1,19 @@ +rockspec_format = "3.0" +package = "panto-codex" +version = "scm-1" + +source = { + url = "git+https://code.tjp.lol/public/panto-codex.git", +} + +description = { + summary = "Codex subscription defaults for panto", + license = "MIT", +} + +dependencies = { "lua >= 5.4, < 5.5" } + +build = { + type = "builtin", + modules = { ["panto-codex"] = "init.lua" }, +} |
