mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-07 13:00:59 +08:00
- Created safe/io.go with ReadAllLimited helper (default 10MB limit) - Replaced 62 unbounded io.ReadAll(resp.Body) calls across 32 files - Covers all exchange traders (Hyperliquid, Bybit, Binance, OKX, Aster, KuCoin, Gate, Bitget, Lighter, Indodax), providers (CoinAnk, Alpaca, TwelveData), MCP client/x402, market data, wallet, telegram, kernel - Prevents OOM from malicious/buggy exchange API responses - Previously fixed: brain.go, sentinel.go already had manual LimitReader
90 lines
2.2 KiB
Go
90 lines
2.2 KiB
Go
package agent
|
|
|
|
import (
|
|
"nofx/safe"
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"nofx/logger"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// apiCallTool executes HTTP requests against the NOFX API server.
|
|
// This is the only tool available to the agent.
|
|
type apiCallTool struct {
|
|
baseURL string
|
|
token string
|
|
client *http.Client
|
|
}
|
|
|
|
// apiRequest holds the arguments decoded from the LLM's api_request tool call.
|
|
type apiRequest struct {
|
|
Method string `json:"method"`
|
|
Path string `json:"path"`
|
|
Body map[string]any `json:"body"`
|
|
}
|
|
|
|
func newAPICallTool(port int, token string) *apiCallTool {
|
|
return &apiCallTool{
|
|
baseURL: fmt.Sprintf("http://127.0.0.1:%d", port),
|
|
token: token,
|
|
client: &http.Client{Timeout: 30 * time.Second},
|
|
}
|
|
}
|
|
|
|
// execute calls the API and returns the response as a string for LLM consumption.
|
|
func (t *apiCallTool) execute(req *apiRequest) string {
|
|
if req.Method == "" || req.Path == "" {
|
|
return "error: method and path are required"
|
|
}
|
|
if !strings.HasPrefix(req.Path, "/") {
|
|
req.Path = "/" + req.Path
|
|
}
|
|
|
|
var bodyReader io.Reader
|
|
if req.Method != "GET" && len(req.Body) > 0 {
|
|
b, err := json.Marshal(req.Body)
|
|
if err != nil {
|
|
return fmt.Sprintf("error marshaling body: %v", err)
|
|
}
|
|
bodyReader = bytes.NewReader(b)
|
|
}
|
|
|
|
httpReq, err := http.NewRequest(req.Method, t.baseURL+req.Path, bodyReader)
|
|
if err != nil {
|
|
return fmt.Sprintf("error creating request: %v", err)
|
|
}
|
|
httpReq.Header.Set("Content-Type", "application/json")
|
|
httpReq.Header.Set("Authorization", "Bearer "+t.token)
|
|
|
|
resp, err := t.client.Do(httpReq)
|
|
if err != nil {
|
|
return fmt.Sprintf("API call failed: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := safe.ReadAllLimited(resp.Body)
|
|
if err != nil {
|
|
return fmt.Sprintf("error reading response: %v", err)
|
|
}
|
|
|
|
logger.Infof("Agent api_call: %s %s -> %d", req.Method, req.Path, resp.StatusCode)
|
|
|
|
if resp.StatusCode >= 400 {
|
|
return fmt.Sprintf("API error %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
// Pretty-print JSON for better LLM readability
|
|
var v any
|
|
if json.Unmarshal(body, &v) == nil {
|
|
if pretty, err := json.MarshalIndent(v, "", " "); err == nil {
|
|
return string(pretty)
|
|
}
|
|
}
|
|
return string(body)
|
|
}
|
|
|