Phase 7 (ADR-0004). Cognitive code asks the router for a role; the router maps roles to configured models with ordered fallbacks, applies global and per-mind daily token budgets and a concurrency limit, records every call (request, response, usage, latency, cost, seed, request hash) and can serve recordings back for exact replay. Providers are raw HTTP clients for OpenAI-compatible, Anthropic and Ollama endpoints, with embedding support for the embed role wired into memory retrieval. Secrets are references only: env:, file:, or enc: values encrypted with AES-256-GCM under NOEMA_MASTER_KEY; keys are resolved at call time and never rendered, logged or exported. Outbound HTTP refuses private and loopback addresses at dial time unless a provider explicitly allows it, bounds redirects and caps bodies. Provider errors are scrubbed. Prompts are named, versioned rows with system and user templates and an optional JSON schema; untrusted text enters only through delimited data blocks. Six defaults are seeded and never overwritten. The UI adds the LLM page (providers, models, role matrix, usage, errors, budgets), a provider page with connectivity test, and a prompt manager with editor, test runner, history, restore, clone and diff. CLI: provider, prompt.
53 lines
1.5 KiB
Go
53 lines
1.5 KiB
Go
package httpclient
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/netip"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestPrivateRanges(t *testing.T) {
|
|
for _, s := range []string{"127.0.0.1", "10.1.2.3", "192.168.1.1", "172.16.5.5", "169.254.169.254", "::1", "fd00::1", "::ffff:10.0.0.1", "0.0.0.0"} {
|
|
if !IsPrivate(netip.MustParseAddr(s)) {
|
|
t.Errorf("%s should be private", s)
|
|
}
|
|
}
|
|
for _, s := range []string{"8.8.8.8", "1.1.1.1", "2606:4700::1111", "93.184.216.34"} {
|
|
if IsPrivate(netip.MustParseAddr(s)) {
|
|
t.Errorf("%s should be public", s)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestClientRefusesLoopbackUnlessAllowed(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/redir" {
|
|
http.Redirect(w, r, "/redir", http.StatusFound)
|
|
return
|
|
}
|
|
w.Write([]byte(strings.Repeat("x", 100))) //nolint:errcheck
|
|
}))
|
|
defer srv.Close()
|
|
strict := New(Options{Timeout: 2 * time.Second})
|
|
_, err := strict.Get(srv.URL)
|
|
if err == nil || !errors.Is(errors.Unwrap(errors.Unwrap(err)), ErrPrivateAddress) && !strings.Contains(err.Error(), "private") {
|
|
t.Fatalf("loopback should be refused by default: %v", err)
|
|
}
|
|
lax := New(Options{Timeout: 2 * time.Second, AllowPrivate: true})
|
|
res, err := lax.Get(srv.URL)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := ReadAll(res.Body, 50); err == nil {
|
|
t.Fatal("body cap not enforced")
|
|
}
|
|
res.Body.Close()
|
|
if _, err := lax.Get(srv.URL + "/redir"); err == nil || !strings.Contains(err.Error(), "redirects") {
|
|
t.Fatalf("redirect loop should be bounded: %v", err)
|
|
}
|
|
}
|