You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@casbin.apache.org by hs...@apache.org on 2026/08/24 13:31:23 UTC
(casbin-gateway) branch master updated: fix(agent): stop Codex asking for OPENAI_API_KEY after a provider switch
This is an automated email from the ASF dual-hosted git repository.
hsluoyz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/casbin-gateway.git
The following commit(s) were added to refs/heads/master by this push:
new 9234c61 fix(agent): stop Codex asking for OPENAI_API_KEY after a provider switch
9234c61 is described below
commit 9234c61729108e2e4ac5285a3018d722507fdb87
Author: Yang Luo <hs...@gmail.com>
AuthorDate: Mon Aug 24 21:31:17 2026 +0800
fix(agent): stop Codex asking for OPENAI_API_KEY after a provider switch
---
agentprovider/codex.go | 118 +++++++++++++++++++++++-------------------
agentprovider/provider.go | 12 +++--
controllers/agent.go | 12 +++--
controllers/agent_provider.go | 22 ++++++++
4 files changed, 102 insertions(+), 62 deletions(-)
diff --git a/agentprovider/codex.go b/agentprovider/codex.go
index cec8a7d..e6537f7 100644
--- a/agentprovider/codex.go
+++ b/agentprovider/codex.go
@@ -28,9 +28,15 @@ const (
// codexProviderName is the model provider Gateway owns in config.toml.
// Everything else in the file, including other providers, is left alone.
codexProviderName = "casbin-gateway"
- // codexAuthKey is where Codex reads the key of a provider whose env_key is
- // not exported in the shell.
+ // codexAuthKey is the auth.json entry an older release wrote the key to.
+ // Codex reads it only while signed in with an API key: a user signed in
+ // through ChatGPT got "Missing environment variable: OPENAI_API_KEY"
+ // instead, so the key now travels in the header below.
codexAuthKey = "OPENAI_API_KEY"
+ // codexAuthHeader carries the key on every request Codex sends to the
+ // provider, which is the one place Codex reads it from without an
+ // environment variable and without a sign-in of its own.
+ codexAuthHeader = "Authorization"
)
// The keys of the root table Gateway owns, remembered under these names.
@@ -38,12 +44,19 @@ const (
codexModelProviderKey = "model_provider"
codexModelKey = "model"
codexAuthStateKey = "auth." + codexAuthKey
+ // codexHeaderStateKey marks a state Gateway wrote without touching
+ // auth.json. A state without it comes from the release that did, and only
+ // that one has an auth.json entry to put back.
+ codexHeaderStateKey = "auth.inHeader"
)
-var codexProviderPath = []string{"model_providers", codexProviderName}
+var (
+ codexProviderPath = []string{"model_providers", codexProviderName}
+ codexHeaderPath = []string{"model_providers", codexProviderName, "http_headers"}
+)
// errCodexNoKey rejects a provider that forwards the caller's own credentials:
-// Codex reads its key from auth.json, so there is nothing for this provider
+// Codex sends the key Gateway writes, so there is nothing for this provider
// entry to forward.
var errCodexNoKey = errors.New("Codex needs an API key, so it cannot use a provider that forwards the credentials of the caller")
@@ -83,15 +96,12 @@ func (w codexWriter) Plan(target Target, endpoint Endpoint) ([]File, error) {
if endpoint.Model != "" {
config = tomlSetRootKey(config, codexModelKey, endpoint.Model)
}
- config = tomlTidy(tomlAppend(config, w.providerTable(endpoint)))
+ preview := endpoint
+ preview.ApiKey = maskSecret(endpoint.ApiKey)
+ config = tomlTidy(tomlAppend(config, w.providerTable(preview)))
- auth, err := encodeJSON(map[string]any{codexAuthKey: maskSecret(endpoint.ApiKey)})
- if err != nil {
- return nil, err
- }
return []File{
{Path: filepath.Join(home, "config.toml"), Format: "toml", Preview: config},
- {Path: filepath.Join(home, "auth.json"), Format: "json", Preview: string(auth)},
}, nil
}
@@ -100,7 +110,7 @@ func (w codexWriter) Apply(target Target, endpoint Endpoint) (map[string]string,
return nil, err
}
- configPath, authPath, err := w.paths(target)
+ configPath, _, err := w.paths(target)
if err != nil {
return nil, err
}
@@ -114,8 +124,11 @@ func (w codexWriter) Apply(target Target, endpoint Endpoint) (map[string]string,
return nil, err
}
- previous := map[string]string{}
- if value, ok := document[codexModelProviderKey].(string); ok {
+ previous := map[string]string{codexHeaderStateKey: "1"}
+ // The CLI and the VS Code integration share one ~/.codex, so the selection
+ // found here can be Gateway's own from the other one, which is not what a
+ // restore should put back.
+ if value, ok := document[codexModelProviderKey].(string); ok && value != codexProviderName {
previous[codexModelProviderKey] = value
}
if value, ok := document[codexModelKey].(string); ok {
@@ -129,28 +142,11 @@ func (w codexWriter) Apply(target Target, endpoint Endpoint) (map[string]string,
}
text = tomlAppend(text, w.providerTable(endpoint))
- auth, _, err := readJSON(authPath)
- if err != nil {
- return nil, err
- }
- if value, ok := auth[codexAuthKey].(string); ok {
- previous[codexAuthStateKey] = value
- }
- auth[codexAuthKey] = endpoint.ApiKey
- authData, err := encodeJSON(auth)
- if err != nil {
- return nil, err
- }
-
changes := &txn{}
if err := changes.write(configPath, []byte(text)); err != nil {
changes.abort()
return nil, err
}
- if err := changes.write(authPath, authData); err != nil {
- changes.abort()
- return nil, err
- }
if err := changes.commit(); err != nil {
return nil, err
}
@@ -169,7 +165,13 @@ func (w codexWriter) Restore(target Target, previous map[string]string) error {
}
text := tomlCutTable(string(data), codexProviderPath...)
for _, key := range []string{codexModelProviderKey, codexModelKey} {
- if value, ok := previous[key]; ok {
+ value, ok := previous[key]
+ // A state that recorded Gateway's own entry, from the installation
+ // sharing this file, would leave the agent switched if it were put back.
+ if key == codexModelProviderKey && value == codexProviderName {
+ ok = false
+ }
+ if ok {
text = tomlSetRootKey(text, key, value)
} else {
text = tomlDeleteRootKey(text, key)
@@ -177,28 +179,34 @@ func (w codexWriter) Restore(target Target, previous map[string]string) error {
}
text = strings.TrimLeft(text, "\n")
- auth, _, err := readJSON(authPath)
- if err != nil {
- return err
- }
- if value, ok := previous[codexAuthStateKey]; ok {
- auth[codexAuthKey] = value
- } else {
- delete(auth, codexAuthKey)
- }
- authData, err := encodeJSON(auth)
- if err != nil {
- return err
- }
-
changes := &txn{}
if err := changes.write(configPath, []byte(text)); err != nil {
changes.abort()
return err
}
- if err := changes.write(authPath, authData); err != nil {
- changes.abort()
- return err
+
+ // Only a state from the release that put the key in auth.json has one to
+ // take back out; a newer one never opened the file.
+ if _, inHeader := previous[codexHeaderStateKey]; !inHeader {
+ auth, _, err := readJSON(authPath)
+ if err != nil {
+ changes.abort()
+ return err
+ }
+ if value, ok := previous[codexAuthStateKey]; ok {
+ auth[codexAuthKey] = value
+ } else {
+ delete(auth, codexAuthKey)
+ }
+ authData, err := encodeJSON(auth)
+ if err != nil {
+ changes.abort()
+ return err
+ }
+ if err := changes.write(authPath, authData); err != nil {
+ changes.abort()
+ return err
+ }
}
return changes.commit()
}
@@ -242,17 +250,21 @@ func (codexWriter) check(endpoint Endpoint) error {
return nil
}
-// providerTable is the [model_providers.casbin-gateway] block. The key lives in
-// auth.json under env_key, so config.toml can be read without exposing it.
+// providerTable is the [model_providers.casbin-gateway] block, followed by the
+// header sub-table carrying the key. env_key would send Codex looking for an
+// environment variable no shell exports, so the key is written here instead.
func (codexWriter) providerTable(endpoint Endpoint) string {
- return tomlTable(codexProviderPath,
- []string{"name", "base_url", "wire_api", "env_key"},
+ table := tomlTable(codexProviderPath,
+ []string{"name", "base_url", "wire_api"},
map[string]string{
"name": "Casbin Gateway",
"base_url": endpoint.BaseUrl,
"wire_api": "responses",
- "env_key": codexAuthKey,
})
+ headers := tomlTable(codexHeaderPath,
+ []string{codexAuthHeader},
+ map[string]string{codexAuthHeader: "Bearer " + endpoint.ApiKey})
+ return table + "\n" + headers
}
func (codexWriter) decode(path string, data []byte) (map[string]any, error) {
diff --git a/agentprovider/provider.go b/agentprovider/provider.go
index 75948ae..5f3a220 100644
--- a/agentprovider/provider.go
+++ b/agentprovider/provider.go
@@ -166,18 +166,20 @@ func Apply(target Target, endpoint Endpoint) error {
// Restore puts back the provider settings the agent had before the first Apply
// and forgets the installation. It is a no-op when Gateway never switched it.
func Restore(target Target) error {
- value, ok := writers[target.AgentId]
- if !ok {
- return fmt.Errorf("%s: %w", target.AgentId, ErrNotSupported)
- }
-
writerMutex.Lock()
defer writerMutex.Unlock()
+ // An installation Gateway never switched has nothing to put back, whether
+ // or not its format is one Gateway can write.
saved, err := loadState(target)
if err != nil || saved == nil {
return err
}
+
+ value, ok := writers[target.AgentId]
+ if !ok {
+ return fmt.Errorf("%s: %w", target.AgentId, ErrNotSupported)
+ }
if err := value.Restore(target, saved.Previous); err != nil {
return err
}
diff --git a/controllers/agent.go b/controllers/agent.go
index fd8fc6a..8dd88bc 100644
--- a/controllers/agent.go
+++ b/controllers/agent.go
@@ -98,7 +98,8 @@ func (c *ApiController) GetAgents() {
// An installation whose configuration file Gateway already wrote is rewritten
// here: in gateway mode the file does not change, which is what makes a switch
// take effect without restarting the agent, but in direct mode the new provider
-// only reaches the agent through its own configuration.
+// only reaches the agent through its own configuration. Unbinding the agent
+// puts that configuration back the way it was found.
func (c *ApiController) UpdateAgentRouting() {
if c.RequireAdmin() {
return
@@ -129,11 +130,14 @@ func (c *ApiController) UpdateAgentRouting() {
return
}
- if form.Provider != "" {
- if failure := reapplyAgentProvider(form.AgentId); failure != "" {
- c.ResponseError("the routing was saved, but the agent configuration was not rewritten: " + failure)
+ if form.Provider == "" {
+ if failure := restoreAgentProvider(form.AgentId); failure != "" {
+ c.ResponseError("the routing was cleared, but the agent configuration was not restored: " + failure)
return
}
+ } else if failure := reapplyAgentProvider(form.AgentId); failure != "" {
+ c.ResponseError("the routing was saved, but the agent configuration was not rewritten: " + failure)
+ return
}
c.ResponseOk(form.Provider)
}
diff --git a/controllers/agent_provider.go b/controllers/agent_provider.go
index 9b5af02..e192adc 100644
--- a/controllers/agent_provider.go
+++ b/controllers/agent_provider.go
@@ -215,6 +215,28 @@ func reapplyAgentProvider(agentId string) string {
return strings.Join(failures, "; ")
}
+// restoreAgentProvider puts back the configuration of every installation
+// Gateway wrote, which is what unbinding an agent means for the files it left
+// behind: they would otherwise keep pointing at a gateway URL that no longer
+// has a provider to forward to.
+func restoreAgentProvider(agentId string) string {
+ installations, err := agent.Scan(false)
+ if err != nil {
+ return err.Error()
+ }
+
+ failures := []string{}
+ for _, installation := range installations {
+ if installation.AgentId != agentId {
+ continue
+ }
+ if err := agentprovider.Restore(providerTarget(targetOf(installation))); err != nil {
+ failures = append(failures, err.Error())
+ }
+ }
+ return strings.Join(failures, "; ")
+}
+
func providerTarget(target agentpatch.Target) agentprovider.Target {
return agentprovider.Target{AgentId: target.AgentId, Path: target.Path, Owner: target.Owner}
}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@casbin.apache.org
For additional commands, e-mail: commits-help@casbin.apache.org