27func privateRequestToken(r *http.Request) string { 28 if a := r.Header.Get("Authorization"); a != "" { 29 return strings.TrimPrefix(a, "Bearer ") 30 } 31 if token := r.Header.Get("X-API-Key"); token != "" { 32 return token 33 } 34 if apiKeyQueryAllowed(r) { 35 return r.URL.Query().Get("api_key") 36 } 37 if appTokenQueryAllowed(r) { 38 token := r.URL.Query().Get("api_key") 39 if strings.HasPrefix(token, "app_") || strings.HasPrefix(token, "dev-") { 40 return token 41 } 42 } 43 return "" 44} 45 46// This is an allowlist, not a GET-method blanket permission. New endpoints, 47// credential exports, arbitrary app/core proxies and protocol upgrades must not 48// silently gain access. Existing handler-level user/project checks still apply. 49func readOnlyAPIRequestAllowed(r *http.Request) bool { 50 if r.Method != http.MethodGet || r.Header.Get("Upgrade") != "" { 51 return false 52 } 53 p := r.URL.Path // authMiddleware runs after /api is stripped 54 if path.Clean(p) != p || strings.ContainsAny(p, "\\%") { 55 return false 56 } 57 switch p { 58 case "/auth/me", "/auth/keys", "/auth/onboarding/status", 59 "/projects", "/agents", "/instances", 60 "/telemetry", "/telemetry/timeline", "/telemetry/stats", 61 "/telemetry/project-activity", "/telemetry/project-stats", 62 "/telemetry/project-timeline", "/telemetry/project-tools", "/telemetry/stream", 63 "/settings/server", "/settings/new-agent-provider": 64 return true 65 } 66 parts := strings.Split(strings.TrimPrefix(p, "/"), "/") 67 if len(parts) == 2 && parts[0] == "projects" && parts[1] != "" { 68 return true 69 } 70 if len(parts) < 2 || (parts[0] != "agents" && parts[0] != "instances") { 71 return false 72 } 73 if id, err := strconv.ParseInt(parts[1], 10, 64); err != nil || id <= 0 { 74 return false 75 } 76 if len(parts) == 2 { 77 return true 78 } 79 if len(parts) != 3 { 80 return false 81 } 82 switch parts[2] { 83 case "status", "threads", "events", "chat-history", "config": 84 return true 85 } 86 return false 87} 88 89// Configuration can contain executable commands, environment variables and 90// provider/MCP credentials. Expose only reviewed scalar settings to read-only 91// callers; arbitrary nested config is intentionally not an inspection surface. 92func readOnlyAgentConfig(config map[string]any) map[string]any { 93 out := map[string]any{} 94 for _, key := range []string{ 95 "directive", "mode", "proactivity", "model", "provider", "default_provider", 96 "rate", "temperature", "max_tokens", "context_size", "reasoning_effort", 97 "background_memory_enabled", "realtime_enabled", 98 } { 99 switch value := config[key].(type) { 100 case string, float64, int, bool: 101 out[key] = value 102 } 103 } 104 return out 105} 106 107func restrictAgentConfig(r *http.Request, agent *Agent) { 108 if !isReadOnlyAPIKey(r) { 109 return 110 } 111 var config map[string]any 112 _ = json.Unmarshal([]byte(agent.Config), &config) 113 raw, _ := json.Marshal(readOnlyAgentConfig(config)) 114 agent.Config = string(raw) 115} 116