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 <[email protected]>
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: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to