summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-10-16 20:13:59 -0600
committert <t@tjp.lol>2026-10-16 20:42:56 -0600
commit3fd3bd6b58b1c9404075c8256ff3ae99fc5b542c (patch)
treec9c0a095e320c6487a65ce5d3529890dd05ec5c9
initial commit; basically working
-rw-r--r--LICENSE18
-rw-r--r--README.md61
-rw-r--r--_selfcheck.lua113
-rw-r--r--init.lua72
-rw-r--r--panto-codex-scm-1.rockspec19
5 files changed, 283 insertions, 0 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..876f03c
--- /dev/null
+++ b/LICENSE
@@ -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" },
+}