feat(gateway): 护栏拦截事件落库 + 安全事件流(T4.B)
- store.GuardrailEvent 表(sundynix_guardrail_event) + AppendGuardrailEvent/ListGuardrailEvents - middleware.Guardrail(db):命中 blocked/suspect 时 best-effort 落库 (actor/kind/reason/signals/method/path/ip,独立超时 ctx) - GET /api/v1/admin/guardrail-events:安全事件流(倒序,翻页) - store.clampPage 抽出分页归一(audit/guardrail 共用) - live:注入 "ignore all previous instructions" → 422 硬拦 + 事件留痕(kind=blocked) - DEPTH_ROADMAP T4.B 护栏事件打勾 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,8 @@ package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
@@ -19,7 +21,8 @@ import (
|
||||
// 只检查带 JSON 体的写请求(POST/PUT);文件上传(multipart)与 GET/SSE 不经此。
|
||||
// 输出护栏不在此做 —— Token 流为 SSE 实时流,网关缓冲会破坏流式,输出过滤应在
|
||||
// dispatcher 的 token 发射层(见 PROGRESS 路线图)。
|
||||
func Guardrail() gin.HandlerFunc {
|
||||
// 命中(blocked/suspect)除打日志外,best-effort 落库 guardrail_event 供安全溯源。
|
||||
func Guardrail(db *store.Postgres) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if m := c.Request.Method; (m == http.MethodPost || m == http.MethodPut) &&
|
||||
strings.HasPrefix(c.GetHeader("Content-Type"), "application/json") {
|
||||
@@ -28,12 +31,14 @@ func Guardrail() gin.HandlerFunc {
|
||||
res := guardrail.Inspect(body)
|
||||
if res.Blocked {
|
||||
log.Printf("[guardrail] 拦截 %s %s:%s", c.Request.Method, c.Request.URL.Path, res.Reason)
|
||||
recordGuardrail(c, db, "blocked", res.Reason, res.Signals)
|
||||
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{"error": "输入护栏拦截:" + res.Reason})
|
||||
return
|
||||
}
|
||||
if res.Suspect { // 灰区:放行但打标,交 Dispatcher 的 LLM 分类器(Tier2)裁决
|
||||
log.Printf("[guardrail] 灰区放行 %s %s:软信号 %v", c.Request.Method, c.Request.URL.Path, res.Signals)
|
||||
c.Set("guardrail_suspect", true)
|
||||
recordGuardrail(c, db, "suspect", res.Reason, res.Signals)
|
||||
}
|
||||
c.Request.Body = io.NopCloser(bytes.NewReader(body)) // 还原请求体供后续 handler 读取
|
||||
}
|
||||
@@ -41,6 +46,28 @@ func Guardrail() gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// recordGuardrail best-effort 落库一条护栏事件(独立超时 ctx,失败静默)。
|
||||
func recordGuardrail(c *gin.Context, db *store.Postgres, kind, reason string, signals []string) {
|
||||
if db == nil {
|
||||
return
|
||||
}
|
||||
uid, _ := c.Get(CtxUserID)
|
||||
actor, _ := uid.(string)
|
||||
sig := ""
|
||||
if len(signals) > 0 {
|
||||
if b, err := json.Marshal(signals); err == nil {
|
||||
sig = string(b)
|
||||
}
|
||||
}
|
||||
e := &store.GuardrailEvent{
|
||||
Actor: actor, Kind: kind, Reason: reason, Signals: sig,
|
||||
Method: c.Request.Method, Path: c.Request.URL.Path, IP: c.ClientIP(),
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
_ = db.AppendGuardrailEvent(ctx, e)
|
||||
}
|
||||
|
||||
// RateLimit 基于 Redis 的会话级限流(按客户端 IP,每分钟上限)。
|
||||
// 上限经 RATE_LIMIT_PER_MIN 配置(缺省 120);压测可调高。Redis 降级时始终放行,不阻断业务。
|
||||
func RateLimit(cache *store.Redis) gin.HandlerFunc {
|
||||
|
||||
Reference in New Issue
Block a user