fix: post-split cleanup — remove test script, fix tests, update docs

- Delete test-mlx.go (standalone test script, not library code)
- Fix TestSandboxing_Symlinks_Followed to match security behaviour
  (renamed to TestSandboxing_Symlinks_Blocked — asserts sandbox
  correctly blocks symlinks escaping the workspace root)
- Fix TestNewTCPTransport_Warning by adding missing security warning
  to NewTCPTransport when binding to 0.0.0.0 (all interfaces)
- Update CLAUDE.md dependency table (go-mlx, duckdb, parquet, ollama,
  qdrant now indirect via go-ml/go-rag)
- Tidy go.mod (direct vs indirect aligned with actual imports)
- Verify go build, go vet, go test all pass

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-02-20 01:18:27 +00:00
parent a36466a870
commit 4665bea75e
7 changed files with 52 additions and 137 deletions

View file

@ -130,7 +130,6 @@ go-ai/
│ ├── tools_chat.go # ide_chat_send/history, ide_session_list/create, ide_plan_status
│ ├── tools_build.go # ide_build_status/list/logs
│ └── tools_dashboard.go # ide_dashboard_overview/activity/metrics
└── test-mlx.go # Standalone scoring pipeline test script
```
## Tool Inventory (49 tools)
@ -152,17 +151,20 @@ go-ai/
## Dependencies
### Direct
| Module | Role |
|--------|------|
| `forge.lthn.ai/core/go` | Core framework: `pkg/io` (filesystem), `pkg/log`, `pkg/process`, `pkg/ws`, `pkg/webview` |
| `forge.lthn.ai/core/go-ml` | ML scoring engine: heuristic scores, judge backend, probes, InfluxDB status |
| `forge.lthn.ai/core/go-rag` | RAG: Qdrant vector DB client, Ollama embeddings, markdown chunking |
| `forge.lthn.ai/core/go-mlx` | Native Metal GPU inference (Apple Silicon) |
| `github.com/modelcontextprotocol/go-sdk` | MCP Go SDK (server, transports, JSON-RPC) |
| `github.com/gorilla/websocket` | WebSocket client for IDE bridge |
| `github.com/marcboeker/go-duckdb` | DuckDB (analytics) |
| `github.com/qdrant/go-client` | Qdrant gRPC client |
| `github.com/ollama/ollama` | Ollama API types |
| `github.com/stretchr/testify` | Test assertions |
### Indirect (transitive via go-ml / go-rag)
`go-mlx`, `go-inference`, `go-duckdb`, `parquet-go`, `ollama`, `qdrant/go-client` are now indirect dependencies pulled in through `go-ml` and `go-rag`. They are not imported directly by go-ai.
All `forge.lthn.ai/core/*` dependencies use `replace` directives pointing to local sibling directories during development.

View file

@ -6,10 +6,10 @@ Virgil dispatches tasks. Mark `[x]` when done, note commit hash.
## Phase 1: Post-Split Cleanup
- [ ] **Remove `test-mlx.go`** — Standalone test script in module root. Not part of the library. Delete it.
- [ ] **Verify `go build ./...` passes** — With replace directives pointing at local clones (via go.work or go.mod). Fix any stale import paths that reference old monolith structure.
- [ ] **Verify `go vet ./...` passes** — Fix any vet warnings.
- [ ] **Run full test suite**`go test ./...` should pass. 84 tests documented in TEST-RESULTS.md. Confirm they still pass after the split.
- [x] **Remove `test-mlx.go`** — Deleted standalone test script from module root.
- [x] **Verify `go build ./...` passes** — Clean build, no stale import paths.
- [x] **Verify `go vet ./...` passes** — No vet warnings.
- [x] **Run full test suite** — All tests pass. Fixed `TestSandboxing_Symlinks_Blocked` (renamed; asserts sandbox blocks symlink escape) and `TestNewTCPTransport_Warning` (added missing security warning to `NewTCPTransport`).
## Phase 2: go-inference Migration

20
go.mod
View file

@ -4,20 +4,16 @@ go 1.25.5
require (
forge.lthn.ai/core/go v0.0.0
forge.lthn.ai/core/go-mlx v0.0.0
forge.lthn.ai/core/go-ml v0.0.0
forge.lthn.ai/core/go-rag v0.0.0
github.com/gorilla/websocket v1.5.3
github.com/marcboeker/go-duckdb v1.8.5
github.com/modelcontextprotocol/go-sdk v1.3.0
github.com/ollama/ollama v0.16.1
github.com/parquet-go/parquet-go v0.27.0
github.com/qdrant/go-client v1.16.2
github.com/stretchr/testify v1.11.1
gopkg.in/yaml.v3 v3.0.1
)
require (
forge.lthn.ai/core/go-inference v0.0.0 // indirect
forge.lthn.ai/core/go-mlx v0.0.0 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/apache/arrow-go/v18 v18.5.1 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
@ -31,11 +27,14 @@ require (
github.com/klauspost/compress v1.18.4 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/mailru/easyjson v0.9.1 // indirect
github.com/marcboeker/go-duckdb v1.8.5 // indirect
github.com/ollama/ollama v0.16.1 // indirect
github.com/parquet-go/bitpack v1.0.0 // indirect
github.com/parquet-go/jsonlite v1.4.0 // indirect
github.com/parquet-go/parquet-go v0.27.0 // indirect
github.com/pierrec/lz4/v4 v4.1.25 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/qdrant/go-client v1.16.2 // indirect
github.com/twpayne/go-geom v1.6.1 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
@ -51,9 +50,10 @@ require (
golang.org/x/text v0.34.0 // indirect
golang.org/x/tools v0.42.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
google.golang.org/grpc v1.78.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/grpc v1.79.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace forge.lthn.ai/core/go => ../core
@ -63,3 +63,5 @@ replace forge.lthn.ai/core/go-mlx => ../go-mlx
replace forge.lthn.ai/core/go-ml => ../go-ml
replace forge.lthn.ai/core/go-rag => ../go-rag
replace forge.lthn.ai/core/go-inference => ../go-inference

34
go.sum
View file

@ -14,6 +14,8 @@ github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPn
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
@ -94,16 +96,16 @@ github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o=
@ -128,12 +130,12 @@ golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View file

@ -149,7 +149,7 @@ func TestSandboxing_Traversal_Sanitized(t *testing.T) {
// should validate inputs before calling Medium.
}
func TestSandboxing_Symlinks_Followed(t *testing.T) {
func TestSandboxing_Symlinks_Blocked(t *testing.T) {
tmpDir := t.TempDir()
outsideDir := t.TempDir()
@ -170,14 +170,11 @@ func TestSandboxing_Symlinks_Followed(t *testing.T) {
t.Fatalf("Failed to create service: %v", err)
}
// Symlinks are followed - no traversal blocking at Medium level.
// This is intentional for simplicity. Callers wanting to block symlinks
// should validate inputs before calling Medium.
content, err := s.medium.Read("link")
if err != nil {
t.Errorf("Expected symlink to be followed, got error: %v", err)
}
if content != "secret" {
t.Errorf("Expected 'secret', got '%s'", content)
// Symlinks pointing outside the sandbox root are blocked (security feature).
// The sandbox resolves the symlink target and rejects it because it escapes
// the workspace boundary.
_, err = s.medium.Read("link")
if err == nil {
t.Error("Expected permission denied for symlink escaping sandbox, but read succeeded")
}
}

View file

@ -26,7 +26,12 @@ type TCPTransport struct {
// NewTCPTransport creates a new TCP transport listener.
// It listens on the provided address (e.g. "localhost:9100").
// Emits a security warning when binding to 0.0.0.0 (all interfaces).
func NewTCPTransport(addr string) (*TCPTransport, error) {
host, _, _ := net.SplitHostPort(addr)
if host == "0.0.0.0" || host == "" {
fmt.Fprintf(os.Stderr, "WARNING: MCP TCP server binding to all interfaces (%s). Use 127.0.0.1 for local-only access.\n", addr)
}
listener, err := net.Listen("tcp", addr)
if err != nil {
return nil, err

View file

@ -1,93 +0,0 @@
// +build ignore
package main
import (
"context"
"fmt"
"os"
"forge.lthn.ai/core/go-ml"
)
func main() {
fmt.Println("=== MLX Backend Test ===")
fmt.Println()
// Test 1: Check if we're on the right platform
fmt.Println("1. Platform check:")
fmt.Printf(" GOOS: %s, GOARCH: %s\n", os.Getenv("GOOS"), os.Getenv("GOARCH"))
fmt.Println()
// Test 2: Try to create backends (without MLX tag, should use HTTP)
fmt.Println("2. Backend availability (without MLX build tag):")
fmt.Println(" Note: MLX backend requires -tags mlx build flag")
fmt.Println()
// Test 3: Check GGUF model directory
fmt.Println("3. GGUF model directory:")
modelDir := "/Volumes/Data/lem/gguf/"
entries, err := os.ReadDir(modelDir)
if err != nil {
fmt.Printf(" Error reading directory: %v\n", err)
} else {
fmt.Printf(" Found %d files in %s\n", len(entries), modelDir)
for _, entry := range entries {
if !entry.IsDir() {
info, _ := entry.Info()
fmt.Printf(" - %s (%.2f GB)\n", entry.Name(), float64(info.Size())/(1024*1024*1024))
}
}
}
fmt.Println()
// Test 4: Test scoring pipeline with mock backend
fmt.Println("4. Testing scoring pipeline:")
// Create a mock backend for testing
mockBackend := &MockBackend{}
// Test heuristic scoring
response := ml.Response{
ID: "test-1",
Prompt: "What is 2+2?",
Response: "The answer to 2+2 is 4. This is a basic arithmetic operation.",
}
hScore := ml.ScoreHeuristic(response.Response)
fmt.Printf(" Heuristic Score: %+v\n", hScore)
// Test judge (without actual model)
judge := ml.NewJudge(mockBackend)
fmt.Printf(" Judge created: %v\n", judge != nil)
// Create scoring engine
engine := ml.NewEngine(judge, 2, "all")
fmt.Printf(" Engine created: %s\n", engine.String())
fmt.Println()
fmt.Println("5. Test probes:")
fmt.Println(" Probes loaded from ml package")
fmt.Println()
fmt.Println("=== Test Complete ===")
}
// MockBackend is a simple backend for testing
type MockBackend struct{}
func (m *MockBackend) Generate(ctx context.Context, prompt string, opts ml.GenOpts) (string, error) {
return `{"score": 5, "reasoning": "Mock response"}`, nil
}
func (m *MockBackend) Chat(ctx context.Context, messages []ml.Message, opts ml.GenOpts) (string, error) {
return `{"score": 5, "reasoning": "Mock response"}`, nil
}
func (m *MockBackend) Name() string {
return "mock"
}
func (m *MockBackend) Available() bool {
return true
}