mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-12 20:15:42 +00:00
* fix(sandbox): avoid shell in FsBridge writes Replace sh -c with interpolated path by shell-free 'tee -- <path>' argv form, piping content via stdin. Prevents command injection through filenames containing shell metacharacters inside the sandbox container. Co-authored-by: evgyur <evgyur@gmail.com> * fix(security): fail-closed on pairing DB errors across channels On IsPaired lookup error, deny instead of granting access. Covers the shared CheckDMPolicy/CheckGroupPolicy helpers (Slack/Discord/Feishu/WhatsApp/Zalo) and the four inline Telegram pairing checks. Co-authored-by: Srini <srinis.k@gmail.com> * fix(security): harden provider URL validation against SSRF Enforce scheme check for all provider types; restrict local types (ollama, claude_cli, acp) to an explicit localhost allowlist instead of skipping checks; resolve remote hostnames and reject any IP in a private/reserved range via the shared security.IsBlocked CIDR list (covers loopback, link-local, metadata, multicast, and unspecified 0.0.0.0/::). Closes the wildcard-DNS bypass and the local-type escape hatch. Operator opt-in via GOCLAW_ALLOW_PRIVATE_PROVIDER_URLS. Exports security.IsBlocked as the single source of truth for blocked ranges. Co-authored-by: Linh Vo Van <linh.vo@e-cq.net> * feat(pipeline): add fail-closed tool call authorization gate Gate tool execution against the server-side AllowedTools allowlist built from the RBAC/tenant-aware filtered tool set. Resolve the tool-call prefix before the allowlist lookup so prefixed agents are not wrongly blocked, re-check deny on lazy MCP activation, and expand IsDenied to cover aliased tool names. Co-authored-by: Huy Doan <tui@pm.me> * fix(security): expand file-serve deny-list defense-in-depth Add absolute-path deny prefixes (/home, /Users, /srv, /var/lib, /var/www, /opt) and an explicit fail-closed log when no file-serving boundary is configured. Co-authored-by: Linh Vo Van <linh.vo@e-cq.net> * fix(providers): allow claude cli executable paths Refs: #1185 --------- Co-authored-by: evgyur <evgyur@gmail.com> Co-authored-by: Srini <srinis.k@gmail.com> Co-authored-by: Linh Vo Van <linh.vo@e-cq.net> Co-authored-by: Huy Doan <tui@pm.me>
158 lines
4.3 KiB
Go
158 lines
4.3 KiB
Go
package providers
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const embeddingBatchSize = 2048
|
|
|
|
// ExpectedEmbeddingDim is the pgvector column dimension used across the system.
|
|
// All embedding providers must return vectors of this dimension.
|
|
const ExpectedEmbeddingDim = 1536
|
|
|
|
// OpenAIEmbeddingProvider implements store.EmbeddingProvider for OpenAI-compatible APIs.
|
|
type OpenAIEmbeddingProvider struct {
|
|
providerName string
|
|
apiKey string
|
|
apiBase string
|
|
model string
|
|
client *http.Client
|
|
retry RetryConfig
|
|
}
|
|
|
|
// NewOpenAIEmbeddingProvider creates an embedding provider for OpenAI-compatible APIs.
|
|
func NewOpenAIEmbeddingProvider(apiKey, apiBase, model string) *OpenAIEmbeddingProvider {
|
|
if apiBase == "" {
|
|
apiBase = "https://api.openai.com/v1"
|
|
}
|
|
if model == "" {
|
|
model = "text-embedding-3-small" // 1536 dimensions, matches pgvector column
|
|
}
|
|
return &OpenAIEmbeddingProvider{
|
|
providerName: "openai",
|
|
apiKey: apiKey,
|
|
apiBase: strings.TrimRight(apiBase, "/"),
|
|
model: model,
|
|
// Use a fixed 60s timeout client. Embedding requests are short but can be
|
|
// batched. The apiBase is validated at provider-creation time by
|
|
// validateProviderURL in internal/http/providers.go.
|
|
client: &http.Client{Timeout: 60 * time.Second},
|
|
retry: DefaultRetryConfig(),
|
|
}
|
|
}
|
|
|
|
func (p *OpenAIEmbeddingProvider) Name() string { return p.providerName }
|
|
func (p *OpenAIEmbeddingProvider) Model() string { return p.model }
|
|
|
|
// Embed generates vector embeddings for the given texts.
|
|
// Returns [][]float32 where each inner slice has ExpectedEmbeddingDim elements.
|
|
func (p *OpenAIEmbeddingProvider) Embed(ctx context.Context, texts []string) ([][]float32, error) {
|
|
if len(texts) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
results := make([][]float32, len(texts))
|
|
|
|
// Process in batches of embeddingBatchSize
|
|
for start := 0; start < len(texts); start += embeddingBatchSize {
|
|
end := min(start+embeddingBatchSize, len(texts))
|
|
|
|
embeddings, err := p.embedBatch(ctx, texts[start:end])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("embedding batch [%d:%d]: %w", start, end, err)
|
|
}
|
|
|
|
for i, emb := range embeddings {
|
|
results[start+i] = emb
|
|
}
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
func (p *OpenAIEmbeddingProvider) embedBatch(ctx context.Context, texts []string) ([][]float32, error) {
|
|
reqBody := map[string]any{
|
|
"model": p.model,
|
|
"input": texts,
|
|
}
|
|
|
|
return RetryDo(ctx, p.retry, func() ([][]float32, error) {
|
|
data, err := json.Marshal(reqBody)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "POST", p.apiBase+"/embeddings", bytes.NewReader(data))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+p.apiKey)
|
|
|
|
resp, err := p.client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("embedding request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return nil, &HTTPError{
|
|
Status: resp.StatusCode,
|
|
Body: string(body),
|
|
RetryAfter: ParseRetryAfter(resp.Header.Get("Retry-After")),
|
|
}
|
|
}
|
|
|
|
var result embeddingResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return nil, fmt.Errorf("decode embedding response: %w", err)
|
|
}
|
|
|
|
// Convert float64 → float32, validate dimension, sort by index
|
|
embeddings := make([][]float32, len(texts))
|
|
for _, d := range result.Data {
|
|
if d.Index >= len(embeddings) {
|
|
continue
|
|
}
|
|
if len(d.Embedding) != ExpectedEmbeddingDim {
|
|
return nil, fmt.Errorf(
|
|
"embedding dimension mismatch: model %s returned %d, expected %d (pgvector column)",
|
|
p.model, len(d.Embedding), ExpectedEmbeddingDim,
|
|
)
|
|
}
|
|
vec := make([]float32, len(d.Embedding))
|
|
for j, v := range d.Embedding {
|
|
vec[j] = float32(v)
|
|
}
|
|
embeddings[d.Index] = vec
|
|
}
|
|
|
|
return embeddings, nil
|
|
})
|
|
}
|
|
|
|
// --- Embedding API response types ---
|
|
|
|
type embeddingResponse struct {
|
|
Data []embeddingData `json:"data"`
|
|
Usage *embeddingUsage `json:"usage,omitempty"`
|
|
}
|
|
|
|
type embeddingData struct {
|
|
Embedding []float64 `json:"embedding"`
|
|
Index int `json:"index"`
|
|
}
|
|
|
|
type embeddingUsage struct {
|
|
PromptTokens int `json:"prompt_tokens"`
|
|
TotalTokens int `json:"total_tokens"`
|
|
}
|