cmd/vps-agent-gateway/main.go
Rama main · réplica de lectura del repositorio original.
package main
import (
"log/slog"
"net"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
"unicode"
portico "github.com/josemirmoura/mcp-vps-agent-gateway"
"github.com/josemirmoura/mcp-vps-agent-gateway/internal/gateway"
"github.com/josemirmoura/mcp-vps-agent-gateway/internal/ipc"
"github.com/josemirmoura/mcp-vps-agent-gateway/internal/securefs"
)
func main() {
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil)))
listen := getenv("VPS_AGENT_LISTEN", "127.0.0.1:8080")
socket := os.Getenv("VPS_AGENT_BROKER_SOCKET")
pocRoot := getenv("VPS_AGENT_POC_ROOT", "/tmp/vps-agent-poc")
var exec gateway.Executor
if socket == "" {
if err := os.MkdirAll(pocRoot, 0o700); err != nil {
slog.Error("fatal", "error", err); os.Exit(1)
}
fs, err := securefs.New([]string{pocRoot}, []string{pocRoot}, securefs.DefaultMaxBytes)
if err != nil {
slog.Error("fatal", "error", err); os.Exit(1)
}
exec = gateway.LocalExecutor{FS: fs}
slog.Info("gateway_local_mode", "safe_root", pocRoot)
} else {
exec = gateway.BrokerExecutor{Client: ipc.Client{Socket: socket, Timeout: 15 * time.Second}}
slog.Info("gateway_broker_configured", "socket", socket)
}
issuer := strings.TrimRight(os.Getenv("VPS_AGENT_OIDC_ISSUER"), "/")
resource := os.Getenv("VPS_AGENT_OAUTH_RESOURCE")
if resource == "" {
resource = os.Getenv("VPS_AGENT_PUBLIC_URL")
}
metadataURL := os.Getenv("VPS_AGENT_RESOURCE_METADATA_URL")
if metadataURL == "" && resource != "" {
metadataURL = metadataURLForResource(resource)
}
authCfg := gateway.AuthConfig{
Mode: getenv("VPS_AGENT_AUTH_MODE", "none"),
StaticToken: os.Getenv("VPS_AGENT_STATIC_TOKEN"),
StaticSubject: getenv("VPS_AGENT_STATIC_SUBJECT", "local-dev"),
RequiredScopes: splitScopes(os.Getenv("VPS_AGENT_REQUIRED_SCOPES")),
ResourceMetadataURL: metadataURL,
ResourceIdentifier: resource,
InstanceID: os.Getenv("VPS_AGENT_INSTANCE_ID"),
InstanceName: getenv("VPS_AGENT_INSTANCE_NAME", "vps-agent"),
MaxConcurrentRequests: getenvInt("VPS_AGENT_MAX_CONCURRENT_REQUESTS", 64),
ToolCallsPerSecond: getenvInt("VPS_AGENT_TOOL_CALLS_PER_SECOND", 20),
ToolCallBurst: getenvInt("VPS_AGENT_TOOL_CALL_BURST", 60),
}
if issuer != "" {
authCfg.AuthorizationServers = []string{issuer}
}
if (authCfg.Mode == "" || authCfg.Mode == "none") && !listenIsLoopback(listen) {
slog.Error("unauthenticated_remote_bind_rejected", "listen", listen, "reason", "auth mode none is allowed only on loopback")
os.Exit(1)
}
if os.Getenv("VPS_AGENT_PUBLIC_URL") != "" && authCfg.Mode != "integrated" {
slog.Error("public_auth_rejected", "reason", "public MCP configuration requires integrated auth"); os.Exit(1)
}
if authCfg.Mode == "integrated" {
if resource == "" || issuer == "" {
slog.Error("integrated_auth_config_invalid", "reason", "OAuth resource/public URL and issuer required"); os.Exit(1)
}
introspectionURL := os.Getenv("VPS_AGENT_INTEGRATED_INTROSPECTION_URL")
// The released Gateway must never use a plaintext introspection
// endpoint. Internal numeric-loopback HTTP is permitted only by
// disposable unit tests that construct NewIntegratedOIDCVerifier directly.
introspectionTarget, parseErr := url.Parse(introspectionURL)
if parseErr != nil || introspectionTarget == nil || introspectionTarget.Scheme != "https" {
slog.Error("integrated_auth_config_invalid", "reason", "HTTPS introspection URL required for distributed Gateway")
os.Exit(1)
}
introspectionHost := os.Getenv("VPS_AGENT_INTEGRATED_INTROSPECTION_HOST")
introspectionClientID := os.Getenv("VPS_AGENT_INTEGRATED_INTROSPECTION_CLIENT_ID")
introspectionClientSecret := os.Getenv("VPS_AGENT_INTEGRATED_INTROSPECTION_CLIENT_SECRET")
expectedAudience := os.Getenv("VPS_AGENT_INTEGRATED_AUDIENCE_PROJECT_ID")
verifier, err := gateway.NewIntegratedOIDCVerifier(
introspectionURL,
introspectionHost,
introspectionClientID,
introspectionClientSecret,
issuer,
expectedAudience,
)
if err != nil {
slog.Error("integrated_auth_config_failed", "error", err); os.Exit(1)
}
authCfg.Verifier = verifier
}
handler := gateway.Handler(exec, authCfg)
srv := &http.Server{
Addr: listen,
Handler: handler,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 30 * time.Second,
WriteTimeout: 0, // Streamable HTTP may legitimately outlive a normal request timeout.
IdleTimeout: 2 * time.Minute,
}
slog.Info("gateway_start", "product_version", portico.Version(), "listen", listen, "auth_mode", authCfg.Mode, "instance_id", authCfg.InstanceID, "instance_name", authCfg.InstanceName)
if err := srv.ListenAndServe(); err != nil { slog.Error("gateway_exit", "error", err); os.Exit(1) }
}
func metadataURLForResource(raw string) string {
u, err := url.Parse(raw)
if err != nil || u.Scheme == "" || u.Host == "" {
return ""
}
return u.Scheme + "://" + u.Host + "/.well-known/oauth-protected-resource"
}
func listenIsLoopback(addr string) bool {
host, _, err := net.SplitHostPort(addr)
if err != nil {
return false
}
if strings.EqualFold(host, "localhost") {
return true
}
ip := net.ParseIP(host)
return ip != nil && ip.IsLoopback()
}
func splitScopes(raw string) []string {
return strings.FieldsFunc(raw, func(r rune) bool {
return r == ',' || unicode.IsSpace(r)
})
}
func getenvInt(name string, fallback int) int {
v := os.Getenv(name)
if v == "" { return fallback }
n, err := strconv.Atoi(v)
if err != nil || n <= 0 { return fallback }
return n
}
func getenv(name, fallback string) string {
if v := os.Getenv(name); v != "" {
return v
}
return fallback
}