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:
1 parent
4a97acf12a
commit
31626c1543
8 files changed
+155
-11
No files matched your search
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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 += `; `
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user