agent: an omp in WSL under NAT gets the gateway's network key, and one bun installed is found and asked its version (whqtian on Discord: a WSL omp connected only once auth: none was swapped for the key by hand, and Kimi K3's max still read xhigh) — a distro not in mirrored networking reaches the gateway at Windows' address (172.23.80.1), where a request without a named key is turned away, yet magpie wrote auth: none there too. Its models.yml now carries the key the gateway is shared on the network with (access.LANSecret: the default LAN key, "" while nothing is shared or it is off) in place of auth: none; an omp on loopback stays keyless. The probe asked omp its version only when sh -l found it on PATH, but bun's and nvm's installers put theirs on PATH in ~/.bashrc, which sh -l doesn't read: the version stayed unknown and max was offered as xhigh, as for an omp older than 16.4.0. It now also looks in ~/.bun/bin, ~/.local/bin, ~/.npm-global/bin, ~/.volta/bin, pnpm's, nvm's, mise's and asdf's folders, and runs --version with the command's folder on PATH, where bun's #!/usr/bin/env bun finds bun. TestOmpKeyBeyondLoopback (NAT address shared or not, loopback, localhost) and TestWSLProbeFindsBunOmp (the probe script run with a bun-installed omp off PATH; fails on the old script: no bin, no version). go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but for TestSavedButSignedOutClaudeServed, which timed out once under load and passes 3 of 3 alone.

This commit is contained in:
yetone committed 2026-10-05 14:40:49 +08:00
1 parent 4a97acf12a
commit 31626c1543
8 files changed
+155 -11

No files matched your search

+28
View File
@@ -92,6 +92,34 @@ func List() ([]Key, error) {
return keys, nil
}
// LANSecret is the key magpie shares the gateway on the local network with
// (the default key ConfigureLAN made), for an agent of this computer's that
// reaches it from beyond loopback — one in a WSL distro under NAT, whose
// requests the gateway takes only with a named key; "" while the gateway
// isn't shared, or that key is off.
func LANSecret() string {
mu.Lock()
defer mu.Unlock()
s := settings.Load()
if !s.LAN {
return ""
}
keys, err := load()
if err != nil {
return ""
}
if i := slices.IndexFunc(keys, func(k Key) bool { return k.ID == s.LANKeyID || k.LAN }); i >= 0 {
if keys[i].Off {
return ""
}
return keys[i].Secret
}
if s.LANKeyID == "" && s.LANKey != "" && !strings.HasPrefix(s.LANKey, revokedLANPrefix) {
return s.LANKey
}
return ""
}
// Export returns credentials only for the encrypted backup bundle.
func Export() ([]Key, error) {
mu.Lock()
+1 -1
View File
@@ -112,7 +112,7 @@ func TestOmoDir(t *testing.T) {
// The WSL probe asks after OmO too.
func TestWSLProbeFindsOmo(t *testing.T) {
for _, want := range []string{`[ -d "$HOME/.omo" ] && echo dir:.omo`, `p=$(command -v omo 2>/dev/null) && echo "bin:omo $p"`} {
for _, want := range []string{`[ -d "$HOME/.omo" ] && echo dir:.omo`, `p=$(command -v omo 2>/dev/null || wslbin omo) && echo "bin:omo $p"`} {
if !strings.Contains(wslProbeScript, want) {
t.Errorf("probe lacks %q", want)
}
+16 -3
View File
@@ -4,7 +4,7 @@ package agent
// (or where ompDir says its variables move it),
// the model of each role under modelRoles as "provider/model", and providers
// of the user's own in models.yml beside it. magpie adds itself there as the
// provider "magpie", keyless (auth: none), with the catalog as its models; a
// provider "magpie", keyless (auth: none) on loopback, with the catalog as its models; a
// model through magpie is "magpie/<provider>/<model>", which omp matches
// whole against provider/id. The other roles, the fallback chains and the
// like may name them as well (ompRefKeys); magpie stays while any does.
@@ -17,6 +17,7 @@ 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"
@@ -386,7 +387,11 @@ type ompThinking struct {
type ompProviderEntry struct {
BaseURL string `yaml:"baseUrl"`
API string `yaml:"api"`
Auth string `yaml:"auth"`
// Auth is none on loopback, where the gateway asks for no key; APIKey,
// for an omp that reaches it from beyond (a WSL distro under NAT), the
// key the gateway is shared on the network with
Auth string `yaml:"auth,omitempty"`
APIKey string `yaml:"apiKey,omitempty"`
// Headers name omp to the gateway: omp 16.x asks with Bun's User-Agent
// and only a later one with its own (omp/18.4.4), so its requests went
// to "Bun" in usage and past omp's own rules and stand-ins
@@ -471,8 +476,16 @@ func ompProviderAt(gw, version string) ompProviderEntry {
}
ms = append(ms, e)
}
return ompProviderEntry{BaseURL: gw + "/v1", API: "openai-completions", Auth: "none",
e := ompProviderEntry{BaseURL: gw + "/v1", API: "openai-completions", Auth: "none",
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
}
}
return e
}
// ompOwnOptions lists the models of the providers the user added to omp's
+96
View File
@@ -0,0 +1,96 @@
package agent
import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/yetone/magpie/internal/access"
"gopkg.in/yaml.v3"
)
// A WSL omp under NAT reaches the gateway at Windows' address, where a
// request without a named key is turned away (whqtian on Discord: it
// connected only once auth: none was swapped for the key by hand). Its
// models.yml carries the key the gateway is shared on the network with;
// one on loopback stays keyless, and so does one while nothing is shared.
func TestOmpKeyBeyondLoopback(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"
if e := ompProviderAt(wsl, "18.4.8"); e.Auth != "none" || e.APIKey != "" {
t.Fatalf("not shared, yet auth %q key %q", e.Auth, e.APIKey)
}
if err := access.ConfigureLAN(true, false); err != nil {
t.Fatal(err)
}
key := access.LANSecret()
if !strings.HasPrefix(key, access.Prefix) {
t.Fatalf("LAN key %q", key)
}
e := ompProviderAt(wsl, "18.4.8")
if e.APIKey != key || e.Auth != "" {
t.Fatalf("WSL omp under NAT: auth %q key %q, want the LAN key", e.Auth, e.APIKey)
}
b, _ := yaml.Marshal(e)
if strings.Contains(string(b), "auth:") || !strings.Contains(string(b), "apiKey: "+key) {
t.Errorf("models.yml entry:\n%s", b)
}
for _, gw := range []string{"http://127.0.0.1:3425", "http://localhost:3425"} {
if e := ompProviderAt(gw, "18.4.8"); e.Auth != "none" || e.APIKey != "" {
t.Errorf("%s: auth %q key %q", gw, e.Auth, e.APIKey)
}
}
if err := access.ConfigureLAN(false, false); err != nil {
t.Fatal(err)
}
if e := ompProviderAt(wsl, "18.4.8"); e.Auth != "none" || e.APIKey != "" {
t.Errorf("sharing off, yet auth %q key %q", e.Auth, e.APIKey)
}
}
// The probe finds an omp bun installed though ~/.bun/bin is put on PATH
// only by ~/.bashrc, which sh -l doesn't read, and asks it its version
// with bun on PATH for its #!/usr/bin/env bun (whqtian on Discord: a WSL
// omp's version stayed unknown, so max stayed xhigh).
func TestWSLProbeFindsBunOmp(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("the probe runs in a Linux distro")
}
to, err := exec.LookPath("timeout")
if err != nil {
t.Skip("no timeout(1)")
}
home := t.TempDir()
bun := filepath.Join(home, ".bun", "bin")
sys := filepath.Join(home, "sys") // PATH: only timeout, nothing of omp's
for _, d := range []string{bun, sys, filepath.Join(home, ".omp")} {
if err := os.MkdirAll(d, 0o755); err != nil {
t.Fatal(err)
}
}
os.WriteFile(filepath.Join(bun, "bun"), []byte("#!/bin/sh\necho omp/16.5.1\n"), 0o755)
os.WriteFile(filepath.Join(bun, "omp"), []byte("#!/usr/bin/env bun\n"), 0o755)
if err := os.Symlink(to, filepath.Join(sys, "timeout")); err != nil {
t.Fatal(err)
}
cmd := exec.Command("/bin/sh", "-c", wslProbeScript)
cmd.Env = []string{"HOME=" + home, "PATH=" + sys + ":/usr/bin:/bin"}
out, err := cmd.Output()
if err != nil {
t.Fatalf("%v: %s", err, out)
}
d := parseProbe("U", string(out))
if d == nil || !d.Has["bin:omp"] || d.Versions["omp"] != "16.5.1" {
t.Fatalf("probe said:\n%s\nread as %+v", out, d)
}
if !ompTakesMax(d.place("omp@wsl:U").version) {
t.Error("its models wouldn't offer max")
}
}
+1 -1
View File
@@ -11,7 +11,7 @@ import (
// One on Windows' drives isn't asked, and one that says no version is
// taken for an older omp as before.
func TestWSLOmpVersionProbed(t *testing.T) {
if !strings.Contains(wslProbeScript, `echo "ver:omp $(timeout 10 "$p" --version`) || !strings.Contains(wslProbeScript, `/mnt/*) ;;`) {
if !strings.Contains(wslProbeScript, `echo "ver:omp $(PATH="${p%/*}:$PATH" timeout 10 "$p" --version`) || !strings.Contains(wslProbeScript, `/mnt/*) ;;`) {
t.Fatalf("the probe doesn't ask omp its version: %s", wslProbeScript)
}
if strings.Contains(wslProbeScript, `"ver:codex`) {
+10 -3
View File
@@ -779,15 +779,22 @@ const wslProbeVersion = 2
// its default route (the Windows host under NAT), and WSL's networking
// mode as wslinfo (WSL 2.0 on) says it; and the version of each that asks
// for it (omp, whose models.yml takes max only from 16.4.0).
//
// A command is also looked for in the folders its installers put it in
// (wslbin): sh -l reads ~/.profile but not ~/.bashrc, where bun's and
// nvm's installers add theirs to PATH (whqtian on Discord: a WSL omp from
// bun was never asked its version, so its models still offered xhigh).
// It is asked with its own folder on PATH, where bun's script finds bun.
var wslProbeScript = func() string {
s := `echo "home:$HOME"; `
s := `wslbin() { for d in "$HOME/.bun/bin" "$HOME/.local/bin" "$HOME/.npm-global/bin" "$HOME/.volta/bin" "$HOME/.local/share/pnpm" "$HOME"/.nvm/versions/node/*/bin "$HOME/.local/share/mise/shims" "$HOME/.asdf/shims"; do [ -x "$d/$1" ] && { echo "$d/$1"; return 0; }; done; return 1; }; `
s += `echo "home:$HOME"; `
for _, k := range wslKinds {
s += `[ -d "$HOME/` + k.dir + `" ] && echo dir:` + k.dir + `; `
if k.bin != "" {
s += `p=$(command -v ` + k.bin + ` 2>/dev/null) && echo "bin:` + k.bin + ` $p"`
s += `p=$(command -v ` + k.bin + ` 2>/dev/null || wslbin ` + k.bin + `) && echo "bin:` + k.bin + ` $p"`
if k.version {
// not one on Windows' drives (WSL's /mnt/c), which isn't the distro's
s += ` && case "$p" in /mnt/*) ;; *) echo "ver:` + k.id + ` $(timeout 10 "$p" --version </dev/null 2>&1 | head -n1)";; esac`
s += ` && case "$p" in /mnt/*) ;; *) echo "ver:` + k.id + ` $(PATH="${p%/*}:$PATH" timeout 10 "$p" --version </dev/null 2>&1 | head -n1)";; esac`
}
s += `; `
}
+1 -1
View File
@@ -44,7 +44,7 @@ func noOwnClaude(t *testing.T) {
// The probe asks after Claude Code as after Codex and Pi, and reads what
// it says.
func TestWSLProbeFindsClaude(t *testing.T) {
for _, want := range []string{`[ -d "$HOME/.claude" ] && echo dir:.claude`, `p=$(command -v claude 2>/dev/null) && echo "bin:claude $p"`,
for _, want := range []string{`[ -d "$HOME/.claude" ] && echo dir:.claude`, `p=$(command -v claude 2>/dev/null || wslbin claude) && echo "bin:claude $p"`,
`[ -d "$HOME/.pi" ] && echo dir:.pi`, `[ -d "$HOME/.codex" ] && echo dir:.codex`} {
if !strings.Contains(wslProbeScript, want) {
t.Errorf("probe lacks %q:\n%s", want, wslProbeScript)
+2 -2
View File
@@ -30,8 +30,8 @@ func piDistroHome(t *testing.T, settings string) (root, home string) {
// The probe asks after Pi as after Codex, and reads what it says.
func TestWSLProbeFindsPi(t *testing.T) {
for _, want := range []string{`[ -d "$HOME/.pi" ] && echo dir:.pi`, `p=$(command -v pi 2>/dev/null) && echo "bin:pi $p"`,
`[ -d "$HOME/.codex" ] && echo dir:.codex`, `p=$(command -v codex 2>/dev/null) && echo "bin:codex $p"`} {
for _, want := range []string{`[ -d "$HOME/.pi" ] && echo dir:.pi`, `p=$(command -v pi 2>/dev/null || wslbin pi) && echo "bin:pi $p"`,
`[ -d "$HOME/.codex" ] && echo dir:.codex`, `p=$(command -v codex 2>/dev/null || wslbin codex) && echo "bin:codex $p"`} {
if !strings.Contains(wslProbeScript, want) {
t.Errorf("probe lacks %q:\n%s", want, wslProbeScript)
}