diff --git a/CLAUDE.md b/CLAUDE.md index e367260..8837fe1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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. diff --git a/TODO.md b/TODO.md index 7fd9da4..1ae23ec 100644 --- a/TODO.md +++ b/TODO.md @@ -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 diff --git a/go.mod b/go.mod index 978f776..77525c1 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 8a9d68d..56f9605 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/mcp/mcp_test.go b/mcp/mcp_test.go index 544d2da..a1701de 100644 --- a/mcp/mcp_test.go +++ b/mcp/mcp_test.go @@ -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") } } diff --git a/mcp/transport_tcp.go b/mcp/transport_tcp.go index 492ef5e..2497d02 100644 --- a/mcp/transport_tcp.go +++ b/mcp/transport_tcp.go @@ -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 diff --git a/test-mlx.go b/test-mlx.go deleted file mode 100644 index 25aa86e..0000000 --- a/test-mlx.go +++ /dev/null @@ -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 -}