agent: every agent in a WSL distro under NAT is given the LAN sharing key, not "magpie" (whqtian on Discord) — v0.1.972 gave it to omp only, and OpenCode in WSL was still written "apiKey": "magpie" with Windows' address, which the gateway takes from loopback only. keyAt(gw) is the key for a gateway URL: the LAN sharing key when the URL isn't this machine's and sharing is on, else gateway.Token; OpenCode (and MiMo Code), Crush, Pi/omo, Codex, Claude Code, Kimi Code, Hermes, Grok, MiniMax Code, Droid, Morph, dsh, Qoder/Qoder CN and omp write it, their Checks compare against it (an old "magpie" there shows as drift), and ourKey still sees a LAN-keyed config as magpie's. TestGatewayKeyBeyondLoopback fails without the change. go vet ./..., go test -tags nogui ./internal/agent ./internal/gui ./internal/gateway ./internal/access ./internal/provider under a temp HOME, GOOS=linux/windows go build pass.

This commit is contained in:
yetone committed 2026-10-05 15:19:09 +08:00
1 parent 2c801b6f9a
commit e77a373792
14 files changed
+161 -46

No files matched your search

+6 -6
View File
@@ -337,7 +337,7 @@ func magpieProviderJSONAt(shape, catalog, gw string) any {
ms[m.ID] = e
}
return map[string]any{"npm": "@ai-sdk/openai-compatible", "name": "magpie",
"options": map[string]any{"baseURL": gw + "/v1", "apiKey": gateway.Token}, "models": ms}
"options": map[string]any{"baseURL": gw + "/v1", "apiKey": keyAt(gw)}, "models": ms}
case "crush":
var ms []map[string]any
for _, m := range models {
@@ -359,7 +359,7 @@ func magpieProviderJSONAt(shape, catalog, gw string) any {
if ms == nil {
ms = []map[string]any{}
}
return map[string]any{"type": "openai", "name": "magpie", "base_url": gw + "/v1", "api_key": gateway.Token, "models": ms}
return map[string]any{"type": "openai", "name": "magpie", "base_url": gw + "/v1", "api_key": keyAt(gw), "models": ms}
case "pi":
var ms []map[string]any
for _, m := range models {
@@ -368,7 +368,7 @@ func magpieProviderJSONAt(shape, catalog, gw string) any {
if ms == nil {
ms = []map[string]any{}
}
return map[string]any{"name": "magpie", "baseUrl": gw + "/v1", "api": "openai-completions", "apiKey": gateway.Token, "models": ms}
return map[string]any{"name": "magpie", "baseUrl": gw + "/v1", "api": "openai-completions", "apiKey": keyAt(gw), "models": ms}
}
return nil
}
@@ -613,7 +613,7 @@ func openCodeLike(at place, id, name, icon, bin, dir, auth string, ua []string,
return ""
}
return wiringOff(name, path, func(k string) (string, bool) { return edit.GetJSON(path, "provider."+magpieID+".options."+k) },
"baseURL", at.v1(), "apiKey", gateway.Token)
"baseURL", at.v1(), "apiKey", at.gwKey())
},
Sync: func() error {
// a model of magpie's chosen, but its provider gone from the
@@ -802,7 +802,7 @@ func piLike(at place, id, name, dir string) *Agent {
return ""
}
return wiringOff(name, modelsPath, func(k string) (string, bool) { return edit.GetJSON(modelsPath, "providers."+magpieID+"."+k) },
"baseUrl", at.v1(), "apiKey", gateway.Token)
"baseUrl", at.v1(), "apiKey", at.gwKey())
},
Sync: func() error {
return syncJSON(modelsPath, "providers."+magpieID, func() any { return magpieProviderJSONAt("pi", id, at.gw()) })
@@ -1158,7 +1158,7 @@ func crushAt(at place, path, data string) *Agent {
return ""
}
return wiringOff("Crush", path, func(k string) (string, bool) { return get("providers." + magpieID + "." + k) },
"base_url", at.v1(), "api_key", gateway.Token)
"base_url", at.v1(), "api_key", at.gwKey())
},
Sync: func() error {
return syncJSON(path, "providers."+magpieID, provider)
+3 -3
View File
@@ -572,7 +572,7 @@ func claudeIn(at place) *Agent {
return err
}
keys := []string{"model"}
if env("ANTHROPIC_AUTH_TOKEN") == gateway.Token {
if ourKey(env("ANTHROPIC_AUTH_TOKEN")) {
// magpie's, left at a gateway address since changed
for _, k := range claudeEnv {
keys = append(keys, "env."+k)
@@ -651,7 +651,7 @@ func claudeIn(at place) *Agent {
}
kvs := []edit.KV{
{Path: "env.ANTHROPIC_BASE_URL", Value: at.gw()},
{Path: "env.ANTHROPIC_AUTH_TOKEN", Value: gateway.Token},
{Path: "env.ANTHROPIC_AUTH_TOKEN", Value: at.gwKey()},
{Path: "env.ANTHROPIC_SMALL_FAST_MODEL", Value: tiers["haiku"]},
{Path: "model", Value: main},
}
@@ -1042,7 +1042,7 @@ func claudeIn(at place) *Agent {
return "Claude Code's managed settings (" + at.native(managed) + ") set ANTHROPIC_BASE_URL to " + u + ", which wins over magpie's"
}
return wiringOff("Claude Code", path, func(k string) (string, bool) { return edit.GetJSON(path, "env."+k) },
"ANTHROPIC_BASE_URL", at.gw(), "ANTHROPIC_AUTH_TOKEN", gateway.Token)
"ANTHROPIC_BASE_URL", at.gw(), "ANTHROPIC_AUTH_TOKEN", at.gwKey())
},
// every prompt typed into Claude Code goes into history.jsonl
LastUsed: func() time.Time {
+3 -3
View File
@@ -148,7 +148,7 @@ func codexIn(at place) *Agent {
edit.KV{Path: "name", Value: "magpie"},
edit.KV{Path: "base_url", Value: at.v1()},
edit.KV{Path: "wire_api", Value: "responses"},
edit.KV{Path: "experimental_bearer_token", Value: gateway.Token},
edit.KV{Path: "experimental_bearer_token", Value: at.gwKey()},
)
}
hasProvider := func() bool {
@@ -739,7 +739,7 @@ func codexIn(at place) *Agent {
if err != nil {
return err.Error()
}
if t["base_url"] != at.v1() || t["experimental_bearer_token"] != gateway.Token || t["wire_api"] != "responses" {
if t["base_url"] != at.v1() || t["experimental_bearer_token"] != at.gwKey() || t["wire_api"] != "responses" {
return "Codex's [model_providers.magpie] no longer points at magpie's gateway (" + at.v1() + ")"
}
if c := get("model_catalog_json"); !ownCatalog(c) {
@@ -1057,7 +1057,7 @@ func codexKeptGateway(path string) string {
}
base, _ := edit.GetTOMLTop(path, "openai_base_url")
var table string
if t, _ := edit.GetTOMLTable(path, "model_providers."+magpieID); t["experimental_bearer_token"] == gateway.Token {
if t, _ := edit.GetTOMLTable(path, "model_providers."+magpieID); ourKey(t["experimental_bearer_token"]) {
table = kept(t["base_url"], "/v1")
}
// the one Codex is on first: magpie's table when it is the provider
+2 -2
View File
@@ -65,7 +65,7 @@ func droidEntries() []droidEntry { return droidEntriesAt(gateway.URL()) }
func droidEntriesAt(gw string) []droidEntry {
var out []droidEntry
for _, m := range magpieModels("droid") {
e := droidEntry{Model: m.ID, ID: droidID + m.ID, BaseURL: gw + "/v1", APIKey: gateway.Token,
e := droidEntry{Model: m.ID, ID: droidID + m.ID, BaseURL: gw + "/v1", APIKey: keyAt(gw),
Provider: "generic-chat-completion-api", MaxContextLimit: m.Context, MaxOutputTokens: maxTokens(m), NoImageSupport: !m.Images}
switch {
case slices.Contains(m.APIs, string(provider.Responses)):
@@ -238,7 +238,7 @@ func droidIn(at place) *Agent {
base = at.gw()
}
return wiringOff("Droid", path, func(k string) (string, bool) { g := r.Get(k); return g.String(), g.Exists() },
"baseUrl", base, "apiKey", gateway.Token)
"baseUrl", base, "apiKey", at.gwKey())
},
Fields: []Field{{
Key: "model", Label: "model",
+10 -10
View File
@@ -141,7 +141,7 @@ func dshAt(at place) *Agent {
if err := dshMirrorHome(dir, nil, gw(), true); err != nil {
return err
}
return dshEnv(dir, false)
return dshEnv(dir, gw(), false)
},
Fields: []Field{{
Key: "model", Label: "model",
@@ -697,11 +697,11 @@ func dshCheck(dir, gw string) string {
}
env := filepath.Join(dir, ".env")
if off := wiringOff("DeepSeek Harness", env, func(k string) (string, bool) { return edit.GetEnvFile(env, k) },
dshKeyRef, gateway.Token); off != "" {
dshKeyRef, keyAt(gw)); off != "" {
return off
}
creds := filepath.Join(dir, ".credentials.yaml")
if v, ok := edit.GetYAML(creds, "refs."+dshKeyRef); ok && v != gateway.Token {
if v, ok := edit.GetYAML(creds, "refs."+dshKeyRef); ok && v != keyAt(gw) {
return "DeepSeek Harness's own key store (" + creds + ") holds another " + dshKeyRef + ", which it uses over magpie's; remove it there (dsh's Models page, Magpie's key) to go through magpie"
}
return ""
@@ -759,7 +759,7 @@ func dshSet(dir, v, gw string) error {
return err
}
}
if err := dshEnv(dir, viaGateway); err != nil {
if err := dshEnv(dir, gw, viaGateway); err != nil {
return err
}
// a model picked in dsh 0.1.x is saved in its settings and goes over
@@ -773,18 +773,18 @@ func dshSet(dir, v, gw string) error {
// dshEnv puts the key for the gateway in $DSH_HOME/.env, or with on false
// takes it out; the name magpie's llm-deepseek entry used goes either way.
func dshEnv(dir string, on bool) error {
func dshEnv(dir, gw string, on bool) error {
env := filepath.Join(dir, ".env")
if on {
if v, ok := edit.GetEnvFile(env, dshKeyRef); !ok || v != gateway.Token {
if err := edit.SetEnvFile(env, edit.KV{Path: dshKeyRef, Value: gateway.Token}); err != nil {
if v, ok := edit.GetEnvFile(env, dshKeyRef); !ok || v != keyAt(gw) {
if err := edit.SetEnvFile(env, edit.KV{Path: dshKeyRef, Value: keyAt(gw)}); err != nil {
return err
}
}
}
for _, k := range []string{dshKeyRef, dshOldKeyRef} {
// the old name only as magpie left it
if v, ok := edit.GetEnvFile(env, k); ok && !(k == dshKeyRef && on) && (k == dshKeyRef || v == gateway.Token) {
if v, ok := edit.GetEnvFile(env, k); ok && !(k == dshKeyRef && on) && (k == dshKeyRef || ourKey(v)) {
if err := edit.DelEnvFile(env, k); err != nil {
return err
}
@@ -1108,7 +1108,7 @@ func dshProviderLines(effort string, models []catalog.Model, gw string) []string
lines := []string{
"- id: llm-deepseek " + dshMark,
" config:",
" apiKey: " + yamlQuote(gateway.Token),
" apiKey: " + yamlQuote(keyAt(gw)),
" baseURL: " + yamlQuote(gw+"/v1"),
" thinking: enabled",
" reasoningEffort: " + effort,
@@ -1190,7 +1190,7 @@ func dshSync(dir, gw string) error {
defer func() {
// the key under the name the route gives it, moved from the old one
if _, items, err := dshRead(files[0]); err == nil && dshWired(items) {
dshEnv(dir, true)
dshEnv(dir, gw, true)
}
}()
for _, f := range files {
+42
View File
@@ -0,0 +1,42 @@
package agent
import (
"github.com/yetone/magpie/internal/access"
"github.com/yetone/magpie/internal/gateway"
)
// keyAt is the key an agent that reaches the gateway at gw sends it. On
// this machine's loopback that is gateway.Token, which the gateway takes
// from this computer only; from another machine — a WSL distro under NAT,
// which asks Windows' address — the gateway turns a request without a
// named key away, so it is the LAN sharing key, while sharing is on
// (whqtian on Discord: omp, then OpenCode, in WSL were refused).
func keyAt(gw string) string {
if onAnotherMachine(gw) {
if k := access.LANSecret(); k != "" {
return k
}
}
return gateway.Token
}
// gwKey is keyAt for the agent at p.
func (p place) gwKey() string { return keyAt(p.gw()) }
// agentKeyAt is keyAt for an agent whose key on this machine names it
// (gateway.TokenFor), for requests that carry nothing else of it.
func agentKeyAt(agent, gw string) string {
if k := keyAt(gw); k != gateway.Token {
return k
}
return gateway.TokenFor(agent)
}
// ourKey says k is a key magpie gives an agent: gateway.Token, or the LAN
// sharing key one that reaches the gateway from elsewhere is given.
func ourKey(k string) bool {
if k == gateway.Token {
return true
}
return k != "" && k == access.LANSecret()
}
+78
View File
@@ -0,0 +1,78 @@
package agent
import (
"path/filepath"
"strings"
"testing"
"github.com/yetone/magpie/internal/access"
"github.com/yetone/magpie/internal/gateway"
)
// An agent in a WSL distro under NAT reaches the gateway at Windows'
// address, where gateway.Token is turned away (whqtian on Discord: OpenCode
// in WSL was written "apiKey": "magpie" and refused). Every agent's config
// carries the LAN sharing key there while sharing is on, and gateway.Token on
// loopback, or while nothing is shared.
func TestGatewayKeyBeyondLoopback(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("USERPROFILE", home)
t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
t.Setenv("XDG_CACHE_HOME", filepath.Join(home, ".cache"))
wsl := "http://172.23.80.1:3425"
local := "http://127.0.0.1:3425"
keys := func(gw string) map[string]string {
oc := magpieProviderJSONAt("opencode", "opencode", gw).(map[string]any)["options"].(map[string]any)
cr := magpieProviderJSONAt("crush", "crush", gw).(map[string]any)
pi := magpieProviderJSONAt("pi", "pi", gw).(map[string]any)
out := map[string]string{
"opencode": oc["apiKey"].(string),
"crush": cr["api_key"].(string),
"pi": pi["apiKey"].(string),
"hermes": hermesProviderAt(gw).APIKey,
}
for _, e := range droidEntriesAt(gw) {
out["droid"] = e.APIKey
}
return out
}
want := func(gw, key string) {
t.Helper()
for agent, k := range keys(gw) {
if k != key {
t.Errorf("%s at %s: key %q, want %q", agent, gw, k, key)
}
}
}
want(wsl, gateway.Token) // nothing shared: nothing else to give
if err := access.ConfigureLAN(true, false); err != nil {
t.Fatal(err)
}
lan := access.LANSecret()
if !strings.HasPrefix(lan, access.Prefix) {
t.Fatalf("LAN key %q", lan)
}
want(wsl, lan)
want(local, gateway.Token)
want("http://localhost:3425", gateway.Token)
if k := qoderProviderAt("qoder", "", wsl+"/v1")["apiKey"]; k != lan {
t.Errorf("Qoder in WSL: key %q, want the LAN key", k)
}
if k := qoderProviderAt("qoder", "", local+"/v1")["apiKey"]; k != gateway.TokenFor("qoder") {
t.Errorf("Qoder on loopback: key %q", k)
}
if !ourKey(lan) || !ourKey(gateway.Token) || ourKey("sk-someone-else") || ourKey("") {
t.Error("ourKey tells magpie's keys from others wrongly")
}
if !qoderKeyed(lan, "qoder") {
t.Error("a Qoder given the LAN key isn't seen as on magpie")
}
if err := access.ConfigureLAN(false, false); err != nil {
t.Fatal(err)
}
want(wsl, gateway.Token)
}
+2 -3
View File
@@ -25,7 +25,6 @@ import (
"github.com/yetone/magpie/internal/catalog"
"github.com/yetone/magpie/internal/edit"
"github.com/yetone/magpie/internal/gateway"
)
// grokEfforts are the reasoning efforts Grok knows.
@@ -43,7 +42,7 @@ func grokModelTables(v1 string) []edit.Table {
{Path: "model", Value: m.ID},
{Path: "name", Value: m.Name},
{Path: "base_url", Value: v1},
{Path: "api_key", Value: gateway.Token},
{Path: "api_key", Value: keyAt(v1)},
{Path: "api_backend", Value: "chat_completions"},
}
if m.Context > 0 {
@@ -153,7 +152,7 @@ func grokIn(at place) *Agent {
return "Grok Build's [model." + strconv.Quote(v) + "] (config.toml) is gone, so it no longer reaches magpie"
}
return wiringOff("Grok Build", path, func(k string) (string, bool) { v, ok := t[k]; return v, ok },
"base_url", at.v1(), "api_key", gateway.Token)
"base_url", at.v1(), "api_key", at.gwKey())
},
Fields: []Field{
{
+2 -2
View File
@@ -69,7 +69,7 @@ func hermesIn(at place) *Agent {
return ""
}
return wiringOff("Hermes", path, func(k string) (string, bool) { return edit.GetYAML(path, "providers."+magpieID+"."+k) },
"base_url", at.v1(), "api_key", gateway.Token)
"base_url", at.v1(), "api_key", at.gwKey())
},
Fields: []Field{{
Key: "model", Label: "model",
@@ -152,7 +152,7 @@ func hermesProviderAt(gw string) hermesProviderEntry {
ms = append(ms, m.ID)
}
return hermesProviderEntry{
Name: magpieID, BaseURL: gw + "/v1", APIKey: gateway.Token, APIMode: "chat_completions",
Name: magpieID, BaseURL: gw + "/v1", APIKey: keyAt(gw), APIMode: "chat_completions",
Headers: map[string]string{"User-Agent": "hermes-agent"}, Models: ms,
}
}
+2 -3
View File
@@ -21,7 +21,6 @@ import (
"strings"
"github.com/yetone/magpie/internal/edit"
"github.com/yetone/magpie/internal/gateway"
)
// kimiModelTable is the header prefix of every model table magpie writes.
@@ -130,7 +129,7 @@ func kimiIn(at place) *Agent {
if err := edit.SetTOMLTable(path, providerTable,
edit.KV{Path: "type", Value: "kimi"},
edit.KV{Path: "base_url", Value: at.v1()},
edit.KV{Path: "api_key", Value: gateway.Token},
edit.KV{Path: "api_key", Value: at.gwKey()},
); err != nil {
return err
}
@@ -191,7 +190,7 @@ func kimiIn(at place) *Agent {
return "Kimi Code's [" + providerTable + "] (config.toml) is gone, so it no longer reaches magpie"
}
return wiringOff("Kimi Code", path, func(k string) (string, bool) { v, ok := t[k]; return v, ok },
"base_url", at.v1(), "api_key", gateway.Token)
"base_url", at.v1(), "api_key", at.gwKey())
},
Fields: []Field{{
Key: "model", Label: "model",
+2 -2
View File
@@ -102,7 +102,7 @@ func miniMaxAt(at place, dir string) *Agent {
return "MiniMax Code's " + mcodeEntry + " (config.yaml) is turned off, so it no longer reaches magpie"
}
return wiringOff("MiniMax Code", path, func(k string) (string, bool) { return edit.GetYAML(path, mcodeEntry+".options."+k) },
"baseURL", at.gw(), "apiKey", gateway.Token)
"baseURL", at.gw(), "apiKey", at.gwKey())
},
Fields: []Field{{
Key: "model", Label: "model",
@@ -183,7 +183,7 @@ func writeMiniMaxEntryAt(path, gw string) error {
// sends them; MiniMax Code's own API is that too
yamlSet(entry, "api", "anthropic-messages")
opts := yamlMap(entry, "options")
yamlSet(opts, "apiKey", gateway.Token)
yamlSet(opts, "apiKey", keyAt(gw))
yamlSet(opts, "baseURL", gw)
yamlSet(opts, "authMode", "api-key")
yamlSet(yamlMap(opts, "headers"), "User-Agent", mcodeUA)
+2 -3
View File
@@ -21,7 +21,6 @@ import (
"strings"
"github.com/yetone/magpie/internal/edit"
"github.com/yetone/magpie/internal/gateway"
)
// morphUA is the User-Agent magpie has morph send, in llm.headers: its
@@ -46,7 +45,7 @@ func morphIn(at place) *Agent {
getKey := func(k string) string { v, _ := edit.GetYAML(path, "llm."+k); return v }
// the token is magpie's alone, where the endpoint moves with the
// gateway's port
onMagpie := func() bool { return getKey("api_key") == gateway.Token }
onMagpie := func() bool { return ourKey(getKey("api_key")) }
// restore puts back what the user had under llm before magpie
restore := func() error {
var del []string
@@ -135,7 +134,7 @@ func morphIn(at place) *Agent {
kvs := []edit.KV{
{Path: "llm.inference_provider", Value: "openai_response_compatible"},
{Path: "llm.endpoint", Value: at.v1()},
{Path: "llm.api_key", Value: gateway.Token},
{Path: "llm.api_key", Value: at.gwKey()},
{Path: "llm.model", Value: ref},
{Path: "llm.headers.User-Agent", Value: morphUA},
}
+2 -5
View File
@@ -17,7 +17,6 @@ import (
"sort"
"strings"
"github.com/yetone/magpie/internal/access"
"github.com/yetone/magpie/internal/appdir"
"github.com/yetone/magpie/internal/catalog"
"github.com/yetone/magpie/internal/edit"
@@ -480,10 +479,8 @@ func ompProviderAt(gw, version string) ompProviderEntry {
Headers: map[string]string{"User-Agent": "omp"}, Models: ms}
// whqtian on Discord: a WSL omp under NAT asks Windows' address, where
// the gateway turns a request without a named key away
if onAnotherMachine(gw) {
if key := access.LANSecret(); key != "" {
e.Auth, e.APIKey = "", key
}
if key := keyAt(gw); key != gateway.Token {
e.Auth, e.APIKey = "", key
}
return e
}
+5 -4
View File
@@ -288,13 +288,14 @@ func qoderProviderAt(agent, model, v1 string) map[string]any {
ms = append(ms, e)
}
return map[string]any{"displayName": "magpie", "protocol": "openai", "baseUrl": v1,
"apiKey": gateway.TokenFor(agent), "model": model, "models": ms}
"apiKey": agentKeyAt(agent, v1), "model": model, "models": ms}
}
// qoderKeyed: key is one magpie gives Qoder's provider — its own,
// gateway.TokenFor, since Qoder's requests carry Bun's User-Agent and
// nothing of Qoder's, or gateway.Token, which it was given before; a sync
// gives it the new one.
// nothing of Qoder's, gateway.Token, which it was given before, or the LAN
// sharing key one in a WSL distro is given (agentKeyAt); a sync gives it
// the new one.
func qoderKeyed(key, agent string) bool {
return key == gateway.TokenFor(agent) || key == gateway.Token
return key == gateway.TokenFor(agent) || ourKey(key)
}