Files
goclaw/internal/http/validate.go
T
Duc Nguyen dc51018563 fix: subagent provider routing + api_base fallback (#262)
* fix(subagent): inherit parent agent's provider instead of alphabetical fallback

Subagents previously used a fixed provider (alphabetically first from the
registry, often "anthropic") regardless of which provider the parent agent
used. This caused invalid combos like anthropic/glm-5 when a zai-coding
agent spawned subagents.

- Pass provider registry to SubagentManager for runtime resolution
- Inject parent provider name into context (WithParentProvider)
- Resolve activeProvider from parent context before LLM call
- Fix trace spans to show actual resolved provider, not default

* fix(providers): api_base fallback from config/env for DB providers

DB providers with empty api_base now inherit from config/env vars
(e.g., GOCLAW_ANTHROPIC_BASE_URL). Prevents proxy API keys from being
sent to the real provider API endpoint.

- Add APIBaseForType() method on ProvidersConfig
- registerProvidersFromDB falls back to config when api_base is empty
- ProvidersHandler uses resolveAPIBase() for model listing
- Add api_base, display_name, settings to provider validation whitelist

* fix(tracing): pass resolved provider name to subagent span emitters

- emitSubagentSpanStart now accepts providerName param instead of
  reading sm.provider.Name() — ensures root subagent span reflects
  the inherited parent provider, not the fallback default
- registerInMemory now uses resolveAPIBase() so DB providers with
  empty api_base inherit the config/env fallback (same as startup path)

---------

Co-authored-by: viettranx <viettranx@gmail.com>
2026-03-18 22:40:49 +07:00

69 lines
2.3 KiB
Go

package http
import (
"log/slog"
"regexp"
)
var slugRe = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]*[a-z0-9])?$`)
// isValidSlug checks whether s matches the slug format: lowercase alphanumeric + hyphens,
// cannot start or end with a hyphen.
func isValidSlug(s string) bool {
return slugRe.MatchString(s)
}
// filterAllowedKeys returns a new map containing only keys present in the allowlist.
// Defense-in-depth: prevents column injection and unauthorized field updates.
func filterAllowedKeys(updates map[string]any, allowed map[string]bool) map[string]any {
filtered := make(map[string]any, len(updates))
for k, v := range updates {
if allowed[k] {
filtered[k] = v
} else {
slog.Warn("security.filtered_unknown_field", "field", k)
}
}
return filtered
}
// --- Field allowlists for update endpoints ---
// Each map lists the columns that HTTP clients may update.
// Immutable fields (id, owner_id, created_at, deleted_at) are excluded.
var agentAllowedFields = map[string]bool{
"agent_key": true, "agent_type": true, "display_name": true,
"provider": true, "model": true, "status": true,
"context_window": true, "max_tool_iterations": true,
"workspace": true, "restrict_to_workspace": true,
"frontmatter": true, "compaction_config": true,
"memory_config": true, "other_config": true, "tools_config": true,
}
var providerAllowedFields = map[string]bool{
"name": true, "provider_type": true, "api_key": true,
"api_base": true, "base_url": true, "default_model": true,
"extra_headers": true, "config": true, "enabled": true,
"display_name": true, "display_order": true, "settings": true,
}
var customToolAllowedFields = map[string]bool{
"name": true, "description": true, "command": true,
"parameters": true, "agent_id": true, "env": true,
"tags": true, "requires": true, "timeout_seconds": true,
"enabled": true,
}
var mcpServerAllowedFields = map[string]bool{
"name": true, "transport": true, "command": true, "args": true,
"url": true, "api_key": true, "env": true, "headers": true,
"enabled": true, "tool_prefix": true, "timeout_seconds": true,
"agent_id": true, "config": true,
}
var channelInstanceAllowedFields = map[string]bool{
"channel_type": true, "credentials": true, "agent_id": true,
"enabled": true, "group_policy": true, "allow_from": true,
"metadata": true, "webhook_secret": true,
}