0.10.1: 安全与业务逻辑加固、新品牌与部署加固
三轮审查修复(60+ 项),相对远端 main(b536672)的关键变更:
- 安全: 数据面 SSRF 拨号防护(防 DNS rebinding)/上游凭据剥离/登录防枚举
与锁定态统一/可信代理(X-Forwarded-For)限流加固/会话版本失效机制/
撤销即时传播/弱密钥拒绝启动/脱敏字节级重写(保签名契约)
- 业务逻辑: 裸 body 上传 panic/bootstrap 审计管线卡死/定价通配符优先级/
全局工具可见性/调度器停机补跑/TOTP 挑战令牌消费顺序/熔断探针语义/
>4MB 响应 token 计量/管理员重置密码作废会话 等
- 前端: 新 logo(语枢 AI 网关主题)/Provider 凭据异常警示/删除入口/
后端错误消息透传/localStorage 敏感数据收敛
- 部署: CREDENTIAL_MASTER_KEY 持久化与弱值拒绝/Provider DELETE 接口/
nginx 安全头/worker 内存限制
- 新增迁移 000029(key_hash 索引)/000030(usage_daily 币种维度)
This commit is contained in:
+37
-2
@@ -10,6 +10,7 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"aigateway.local/core/internal/agentnode"
|
||||
"aigateway.local/core/internal/apikey"
|
||||
"aigateway.local/core/internal/audit"
|
||||
"aigateway.local/core/internal/contentpolicy"
|
||||
@@ -23,15 +24,17 @@ import (
|
||||
"aigateway.local/core/internal/platform/cryptox"
|
||||
"aigateway.local/core/internal/platform/database"
|
||||
"aigateway.local/core/internal/platform/health"
|
||||
"aigateway.local/core/internal/platform/storage"
|
||||
"aigateway.local/core/internal/platform/httpserver"
|
||||
"aigateway.local/core/internal/platform/storage"
|
||||
"aigateway.local/core/internal/portal"
|
||||
"aigateway.local/core/internal/pricing"
|
||||
"aigateway.local/core/internal/provider"
|
||||
providercontrolplane "aigateway.local/core/internal/provider/controlplane"
|
||||
provideropenai "aigateway.local/core/internal/provider/openai"
|
||||
providerruntime "aigateway.local/core/internal/provider/runtime"
|
||||
"aigateway.local/core/internal/scheduler"
|
||||
"aigateway.local/core/internal/shadow"
|
||||
"aigateway.local/core/internal/trace"
|
||||
"aigateway.local/core/internal/workbench"
|
||||
)
|
||||
|
||||
@@ -129,6 +132,7 @@ func main() {
|
||||
apiKeyAuthenticator := apikey.NewAuthenticator(apiKeyRepository, criticalRedis, bootstrapAPIKey)
|
||||
apiKeyAuthenticator.SetLogger(logger)
|
||||
proxy := gateway.NewDynamicProxy(providerResolver, apiKeyAuthenticator, cfg.Server.MaxBodyBytes, logger)
|
||||
proxy.SetAllowPrivateProviderURLs(cfg.Credentials.AllowPrivateProviderURL)
|
||||
proxy.SetAdmissionController(gateway.NewRedisAdmissionController(criticalRedis))
|
||||
proxy.SetTokenQuotaController(gateway.NewRedisTokenQuotaController(criticalRedis))
|
||||
proxy.SetResiliencePolicy(gateway.ResiliencePolicy{
|
||||
@@ -160,7 +164,7 @@ func main() {
|
||||
proxy.SetPricingService(pricingService)
|
||||
identityRepository := identity.NewRepository(db)
|
||||
sessionStore := identity.NewSessionStore(criticalRedis, cfg.Auth.SessionTTL)
|
||||
loginLimiter := identity.NewLoginLimiter(criticalRedis, cfg.Auth.LoginRateLimitMax, cfg.Auth.LoginRateLimitWindow)
|
||||
loginLimiter := identity.NewLoginLimiter(criticalRedis, cfg.Auth.LoginRateLimitMax, cfg.Auth.LoginRateLimitWindow, cfg.Auth.TrustedProxies)
|
||||
totpCipher, err := cryptox.NewKeyring(
|
||||
cfg.Credentials.MasterKey, cfg.Credentials.KEKVersion, cfg.Credentials.KEKKeyring, "totp-secret",
|
||||
)
|
||||
@@ -268,6 +272,24 @@ func main() {
|
||||
fileService := workbench.NewFileService(workbenchService, objectStore)
|
||||
filesAdminHandler := workbench.NewFilesAdminHTTPHandler(fileService, identityService)
|
||||
filesPortalHandler := workbench.NewFilesPortalHTTPHandler(fileService, identityService)
|
||||
// M8 P4:站内消息。未读数以 PostgreSQL 为权威源,inbox service 仅用 Redis PUBLISH
|
||||
// 提醒订阅方;通知 worker 在同一消费循环内物化事件(见 gateway-notification-worker)。
|
||||
inboxService := workbench.NewInboxService(workbenchService, criticalRedis, cfg.Inbox.Channel)
|
||||
inboxAdminHandler := workbench.NewInboxAdminHTTPHandler(inboxService, identityService)
|
||||
inboxPortalHandler := workbench.NewInboxPortalHTTPHandler(inboxService, identityService)
|
||||
schedulerCipher, err := cryptox.NewKeyring(
|
||||
cfg.Credentials.MasterKey, cfg.Credentials.KEKVersion, cfg.Credentials.KEKKeyring, "scheduled-task-api-key",
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error("scheduled task encryption initialization failed", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
schedulerService := scheduler.NewService(db, schedulerCipher)
|
||||
schedulerHandler := scheduler.NewAdminHTTPHandler(schedulerService, identityService)
|
||||
traceStore := trace.NewStore(db)
|
||||
traceHandler := trace.NewAdminHTTPHandler(traceStore, identityService)
|
||||
agentNodeStore := agentnode.NewStore(db)
|
||||
agentNodeHandler := agentnode.NewHTTPHandler(agentNodeStore, identityService)
|
||||
applicationKeyCipher, err := cryptox.NewKeyring(
|
||||
cfg.Credentials.MasterKey, cfg.Credentials.KEKVersion, cfg.Credentials.KEKKeyring, "application-runtime-key",
|
||||
)
|
||||
@@ -285,6 +307,7 @@ func main() {
|
||||
MCPClient: mcpClient,
|
||||
})
|
||||
workbenchRuntime.SetLogger(logger)
|
||||
workbenchRuntime.SetTraceStore(traceStore)
|
||||
// Wire the fact-check engine: the admin fact-check settings/policies UI now
|
||||
// actually governs application answers instead of being inert configuration.
|
||||
factCheckEngine := factcheck.NewEngine(db, workbench.NewFactCheckRetriever(workbench.NewRetriever(workbenchService, workbenchService.Embedder())), logger)
|
||||
@@ -348,6 +371,18 @@ func main() {
|
||||
controlMux.Handle("/api/v1/admin/files/", filesAdminHandler)
|
||||
controlMux.Handle("/api/v1/portal/files", filesPortalHandler)
|
||||
controlMux.Handle("/api/v1/portal/files/", filesPortalHandler)
|
||||
controlMux.Handle("/api/v1/admin/inbox", inboxAdminHandler)
|
||||
controlMux.Handle("/api/v1/admin/inbox/", inboxAdminHandler)
|
||||
controlMux.Handle("/api/v1/portal/inbox", inboxPortalHandler)
|
||||
controlMux.Handle("/api/v1/portal/inbox/", inboxPortalHandler)
|
||||
controlMux.Handle("/api/v1/admin/scheduled-tasks", schedulerHandler)
|
||||
controlMux.Handle("/api/v1/admin/scheduled-tasks/", schedulerHandler)
|
||||
controlMux.Handle("/api/v1/admin/traces", traceHandler)
|
||||
controlMux.Handle("/api/v1/admin/traces/", traceHandler)
|
||||
controlMux.Handle("/api/v1/admin/agent-sessions", traceHandler)
|
||||
controlMux.Handle("/api/v1/admin/agent-nodes", agentNodeHandler)
|
||||
controlMux.Handle("/api/v1/admin/agent-nodes/", agentNodeHandler)
|
||||
controlMux.Handle("/api/v1/agent/nodes/", agentNodeHandler)
|
||||
controlMux.Handle("/api/v1/admin/reload", operationsHandler)
|
||||
controlMux.Handle("/api/v1/admin/identities/", identityManagementHandler)
|
||||
controlMux.Handle("/api/v1/admin/departments", identityManagementHandler)
|
||||
|
||||
@@ -32,7 +32,7 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
defer db.Close()
|
||||
maintenance := audit.NewMaintenance(db, cfg.Audit.Retention, cfg.Audit.UsageRetention, cfg.Audit.PartitionMonthsAhead)
|
||||
maintenance := audit.NewMaintenance(db, cfg.Audit.Retention, cfg.Audit.UsageRetention, cfg.Audit.TraceRetention, cfg.Audit.PartitionMonthsAhead)
|
||||
ticker := time.NewTicker(cfg.Audit.MaintenanceInterval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
@@ -40,7 +40,7 @@ func main() {
|
||||
if runErr != nil && ctx.Err() == nil {
|
||||
logger.Error("audit maintenance failed", "error", runErr)
|
||||
} else if runErr == nil {
|
||||
logger.Info("audit maintenance complete", "created_partitions", result.CreatedPartitions, "dropped_partitions", result.DroppedPartitions, "deleted_audit_rows", result.DeletedAuditRows, "deleted_usage_rows", result.DeletedUsageRows)
|
||||
logger.Info("audit maintenance complete", "created_partitions", result.CreatedPartitions, "dropped_partitions", result.DroppedPartitions, "deleted_audit_rows", result.DeletedAuditRows, "deleted_usage_rows", result.DeletedUsageRows, "deleted_trace_rows", result.DeletedTraceRows)
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
|
||||
@@ -50,8 +50,11 @@ func main() {
|
||||
logger.Error("consumer ID generation failed", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
service := workbench.NewNotificationService(workbench.NewService(db), cipher, cfg.Credentials.AllowPrivateWebhookURL)
|
||||
assets := workbench.NewService(db)
|
||||
service := workbench.NewNotificationService(assets, cipher, cfg.Credentials.AllowPrivateWebhookURL)
|
||||
dispatcher := workbench.NewNotificationDispatcher(service, client, cfg.Outbox.Stream, "notification-"+consumer, logger)
|
||||
// M8 P4:站内消息由通知 worker 在同一消费循环内物化,不新增第 5 个 worker。
|
||||
dispatcher.SetInbox(workbench.NewInboxService(assets, client, cfg.Inbox.Channel))
|
||||
logger.Info("notification worker started", "consumer", consumer, "stream", cfg.Outbox.Stream)
|
||||
if err = dispatcher.Run(ctx); err != nil {
|
||||
logger.Error("notification worker stopped unexpectedly", "error", err)
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"aigateway.local/core/internal/platform/config"
|
||||
"aigateway.local/core/internal/platform/cryptox"
|
||||
"aigateway.local/core/internal/platform/database"
|
||||
platformid "aigateway.local/core/internal/platform/id"
|
||||
"aigateway.local/core/internal/scheduler"
|
||||
)
|
||||
|
||||
func main() {
|
||||
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
logger.Error("invalid configuration", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
db, err := database.Open(ctx, cfg.Database)
|
||||
if err != nil {
|
||||
logger.Error("database initialization failed", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer db.Close()
|
||||
cipher, err := cryptox.NewKeyring(cfg.Credentials.MasterKey, cfg.Credentials.KEKVersion, cfg.Credentials.KEKKeyring, "scheduled-task-api-key")
|
||||
if err != nil {
|
||||
logger.Error("scheduled task cipher initialization failed", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
workerID, err := platformid.NewUUID()
|
||||
if err != nil {
|
||||
logger.Error("worker ID generation failed", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
service := scheduler.NewService(db, cipher)
|
||||
engine := scheduler.NewEngine(service, cfg.Scheduler.GatewayBaseURL, "scheduler-"+workerID, cfg.Scheduler.BatchSize, cfg.Scheduler.MaxAttempts, cfg.Scheduler.ExecutionTimeout, logger)
|
||||
ticker := time.NewTicker(cfg.Scheduler.PollInterval)
|
||||
defer ticker.Stop()
|
||||
logger.Info("scheduler worker started", "worker_id", workerID, "poll_interval", cfg.Scheduler.PollInterval)
|
||||
for {
|
||||
processed, tickErr := engine.Tick(ctx, time.Now())
|
||||
if tickErr != nil && ctx.Err() == nil {
|
||||
logger.Error("scheduler tick failed", "error", tickErr)
|
||||
} else if processed > 0 {
|
||||
logger.Info("scheduler batch complete", "processed", processed)
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
logger.Info("scheduler worker stopped")
|
||||
return
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user