Skip to content

Commit a914d20

Browse files
authored
Catalog refactor and security/quality hardening pass (#59)
* feat: add Poolside provider (constant, profile, detection order, env keys) * feat: wire Groq and Poolside providers end-to-end (config, registry, deployment, onboarding) * refactor: consolidate version into root version.go with //go:embed VERSION - Remove broken internal/version/ package (embedding stale 0.1.0 copy) - Add root version.go with //go:embed VERSION (resolves to root VERSION) - Revert client/client.go to SetVersion pattern (version set by root init) - Remove legacy migration code (migrate.go, EnsureDeploymentConfigV2, etc.) - Add deploymentOwnerProviderID for poolside/groq-direct routing * fix: update Poolside default base URL to inference.poolside.ai api.poolside.ai does not resolve — Poolside runs on Baseten (inference.poolside.ai). Verified with curl that models endpoint returns available models (laguna-m.1, laguna-xs.2). * fix: add live fetcher functions for Poolside and Groq Both providers were missing FetchPoolside and FetchGroq functions and were not registered in the live.Registry map, causing 'live: unknown fetcher' errors during catalog discovery. Also updates the parity test assertion from 19 to 21 providers. * test: add live fetcher tests for Poolside and Groq Adds mock HTTP server tests, no-key tests, and JSON parsing tests for both FetchPoolside and FetchGroq, following the same pattern as existing provider tests (grok, kimi, etc.). * feat: add ClinePass provider Adds ClinePass as an OpenAI-compatible provider using the https://api.cline.bot/api/v1 endpoint with CLINE_API_KEY auth. Wires through all layers: provider spec, env config, profile, runtime profile, routing, live fetcher, client registry, compat config, deployment, and tests. * fix: set ClinePass probe to ProbeNone, use curated model list Cline API does not expose a GET /models endpoint, so credential probing fails with HTTP 404. Changed ProbeKind to ProbeNone (key is saved without validation). Live fetcher returns a curated static list of 10 known ClinePass models instead of hitting the API. * feat: add Poolside Laguna M.1 free model to ClinePass curated list * feat: use ClinePass reference pricing, poolside stays free * fix: add missing legacyDeploymentAndOwner entries for ClinePass, Poolside, Groq, MiniMax * Catalog refactor and security/quality hardening pass - Add fuzz CI job for guardrails, message sanitization, and cache-key fuzz targets - Catalog v1 spec parsing, provider discovery/refresh, live enrichment, and deployment routing refactor - Remove deprecated legacy catalog shim * Apply gofumpt formatting to satisfy CI format check
1 parent 949f6e0 commit a914d20

4 files changed

Lines changed: 38 additions & 4 deletions

File tree

.github/workflows/ci.yml

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,28 @@ jobs:
248248
markdownlint-cli2 '**/*.md'
249249
250250
# -------------------------------------------------------------------------
251-
# 9. Cross-platform build matrix — zero CGO, all targets.
251+
# 9. Fuzz the message-sanitization and cache-key helpers that consume
252+
# untrusted/adversarial LLM conversation content.
253+
# -------------------------------------------------------------------------
254+
fuzz:
255+
name: fuzz (60s)
256+
runs-on: ubuntu-latest
257+
needs: [test]
258+
steps:
259+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
260+
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
261+
with:
262+
go-version: ${{ env.GO_VERSION }}
263+
cache: true
264+
- name: Run fuzz targets
265+
run: |
266+
go test -fuzz=FuzzSanitizeMessages -fuzztime=60s ./client
267+
go test -fuzz=FuzzMergeConsecutiveRoles -fuzztime=60s ./client
268+
go test -fuzz=FuzzBuildCacheKey -fuzztime=60s ./client
269+
go test -fuzz=FuzzGuardrailsCheck -fuzztime=60s ./client
270+
271+
# -------------------------------------------------------------------------
272+
# 10. Cross-platform build matrix — zero CGO, all targets.
252273
# -------------------------------------------------------------------------
253274
build:
254275
name: build (${{ matrix.goos }}/${{ matrix.goarch }})

catalog/live/fetchers_providers.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -621,7 +621,7 @@ func FetchClinePass(env map[string]string) ([]Entry, error) {
621621
{"cline-pass/mimo-v2.5-pro", "MiMo V2.5 Pro", "xiaomi", 131072, 8192, 1.74, 3.48},
622622
{"cline-pass/mimo-v2.5", "MiMo V2.5", "xiaomi", 131072, 8192, 0.14, 0.28},
623623
{"cline-pass/qwen3.7-max", "Qwen 3.7 Max", "qwen", 131072, 8192, 2.50, 7.50},
624-
{"cline-pass/qwen3.7-plus", "Qwen 3.7 Plus", "qwen", 131072, 8192, 0.40, 1.60},
624+
{"cline-pass/qwen3.7-plus", "Qwen 3.7 Plus", "qwen", 131072, 8192, 0.40, 1.28},
625625
{"cline-pass/poolside-laguna-m.1-free", "Poolside Laguna M.1 (Free)", "poolside", 262144, 32768, 0, 0},
626626
}
627627
var entries []Entry

catalog/registry/providers.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,23 +174,31 @@ func providerSpecs() []ProviderSpec {
174174
BaseURLEnv: []string{"POOLSIDE_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"},
175175
ProbeKind: ProbeOpenAIModels, ProbeBaseURL: "https://inference.poolside.ai/v1",
176176
LiveFetcherKey: "poolside", LiveCatalogKey: "poolside",
177-
ProtocolID: "openai-chat-completions", AdapterID: "poolside", RuntimeProfileKey: "poolside",
177+
APIProtocolID: "openai-chat-completions", AdapterID: "poolside", RuntimeProfileKey: "poolside",
178178
},
179179
{
180180
ProviderID: "groq", DisplayName: "Groq", DeploymentID: "groq-direct", SortOrder: 19, ChatPreference: 21,
181181
RequiresKey: true, CredentialEnv: "GROQ_API_KEY",
182182
BaseURLEnv: []string{"GROQ_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"},
183183
ProbeKind: ProbeOpenAIModels, ProbeBaseURL: "https://api.groq.com/openai/v1",
184184
LiveFetcherKey: "groq", LiveCatalogKey: "groq",
185+
<<<<<<< HEAD
186+
APIProtocolID: "openai-chat-completions", AdapterID: "groq", RuntimeProfileKey: "groq",
187+
=======
185188
ProtocolID: "openai-chat-completions", AdapterID: "groq", RuntimeProfileKey: "groq",
189+
>>>>>>> origin/main
186190
},
187191
{
188192
ProviderID: "clinepass", DisplayName: "ClinePass", DeploymentID: "clinepass", SortOrder: 20, ChatPreference: 22,
189193
RequiresKey: true, CredentialEnv: "CLINE_API_KEY",
190194
BaseURLEnv: []string{"CLINE_API_BASE", "OPENAI_BASE_URL", "OPENAI_API_BASE"},
191195
ProbeKind: ProbeNone,
192196
LiveFetcherKey: "clinepass", LiveCatalogKey: "clinepass",
197+
<<<<<<< HEAD
198+
APIProtocolID: "openai-chat-completions", AdapterID: "clinepass", RuntimeProfileKey: "clinepass",
199+
=======
193200
ProtocolID: "openai-chat-completions", AdapterID: "clinepass", RuntimeProfileKey: "clinepass",
201+
>>>>>>> origin/main
194202
},
195203
{
196204
ProviderID: "opencodego", DisplayName: "OpenCode Go", DeploymentID: "opencodego", SortOrder: 21, ChatPreference: 13,

catalog/xiaomi/platform.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ import (
1414
// Inference GET {base}/models only returns id/object/owned_by.
1515
const DefaultPlatformModelsURL = "https://platform.xiaomimimo.com/api/v1/models"
1616

17+
// maxPlatformResponseBytes caps how much of the platform catalog response is
18+
// read, so a malicious or misconfigured endpoint (including the env-overridable
19+
// XIAOMI_MIMO_PLATFORM_MODELS_URL) cannot exhaust memory via an unbounded body.
20+
const maxPlatformResponseBytes = 10 * 1024 * 1024 // 10 MiB
21+
1722
var platformHTTPClient = &http.Client{Timeout: 30 * time.Second}
1823

1924
// PlatformModel is one row from the platform catalog API.
@@ -65,7 +70,7 @@ func FetchPlatformModelsIndex(ctx context.Context, catalogURL string) (map[strin
6570
return nil, fmt.Errorf("xiaomi platform catalog: %w", err)
6671
}
6772
defer func() { _ = resp.Body.Close() }()
68-
body, err := io.ReadAll(resp.Body)
73+
body, err := io.ReadAll(io.LimitReader(resp.Body, maxPlatformResponseBytes))
6974
if err != nil {
7075
return nil, err
7176
}

0 commit comments

Comments
 (0)