140 lines
4.1 KiB
Go
140 lines
4.1 KiB
Go
package securityaudit
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"errors"
|
||
|
|
"net/http"
|
||
|
|
"sync"
|
||
|
|
)
|
||
|
|
|
||
|
|
type LegacyEngine interface {
|
||
|
|
Check(ctx context.Context, req Request) (*LegacyDecision, error)
|
||
|
|
}
|
||
|
|
|
||
|
|
type PromptEngine interface {
|
||
|
|
EffectiveMode() Mode
|
||
|
|
Enqueue(ctx context.Context, req Request) error
|
||
|
|
Evaluate(ctx context.Context, req Request) (*PromptDecision, error)
|
||
|
|
}
|
||
|
|
|
||
|
|
type Coordinator struct {
|
||
|
|
legacy LegacyEngine
|
||
|
|
prompt PromptEngine
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewCoordinator(legacy LegacyEngine, prompt PromptEngine) *Coordinator {
|
||
|
|
return &Coordinator{legacy: legacy, prompt: prompt}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (c *Coordinator) Check(ctx context.Context, req Request) Decision {
|
||
|
|
if c == nil {
|
||
|
|
return allowDecision(nil, nil)
|
||
|
|
}
|
||
|
|
mode := ModeOff
|
||
|
|
if c.prompt != nil {
|
||
|
|
mode = c.prompt.EffectiveMode()
|
||
|
|
}
|
||
|
|
switch mode {
|
||
|
|
case ModeAsync:
|
||
|
|
// Enqueue is deliberately best-effort. The implementation owns a bounded
|
||
|
|
// context and copies request memory before it can outlive the Handler.
|
||
|
|
_ = c.prompt.Enqueue(ctx, req.Clone())
|
||
|
|
legacy, _ := c.checkLegacy(ctx, req)
|
||
|
|
return prioritize(legacy, nil)
|
||
|
|
case ModeBlocking:
|
||
|
|
return c.checkBlocking(ctx, req)
|
||
|
|
default:
|
||
|
|
legacy, _ := c.checkLegacy(ctx, req)
|
||
|
|
return prioritize(legacy, nil)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (c *Coordinator) checkBlocking(ctx context.Context, req Request) Decision {
|
||
|
|
var wg sync.WaitGroup
|
||
|
|
wg.Add(2)
|
||
|
|
var legacy *LegacyDecision
|
||
|
|
var prompt *PromptDecision
|
||
|
|
go func() {
|
||
|
|
defer wg.Done()
|
||
|
|
legacy, _ = c.checkLegacy(ctx, req)
|
||
|
|
}()
|
||
|
|
go func() {
|
||
|
|
defer wg.Done()
|
||
|
|
if c.prompt == nil {
|
||
|
|
prompt = unavailablePromptDecision(ErrorCodeUnavailable)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
result, err := c.prompt.Evaluate(ctx, req.Clone())
|
||
|
|
if err != nil {
|
||
|
|
var guardErr *GuardError
|
||
|
|
if errors.As(err, &guardErr) && guardErr.Code == ErrorCodeInvalidResponse {
|
||
|
|
prompt = unavailablePromptDecision(ErrorCodeInvalidResponse)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
prompt = unavailablePromptDecision(ErrorCodeUnavailable)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if result == nil {
|
||
|
|
prompt = unavailablePromptDecision(ErrorCodeUnavailable)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
prompt = result
|
||
|
|
}()
|
||
|
|
wg.Wait()
|
||
|
|
return prioritize(legacy, prompt)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (c *Coordinator) checkLegacy(ctx context.Context, req Request) (*LegacyDecision, error) {
|
||
|
|
if c.legacy == nil {
|
||
|
|
return nil, nil
|
||
|
|
}
|
||
|
|
return c.legacy.Check(ctx, req)
|
||
|
|
}
|
||
|
|
|
||
|
|
func prioritize(legacy *LegacyDecision, prompt *PromptDecision) Decision {
|
||
|
|
if legacy != nil && legacy.Blocked {
|
||
|
|
status := legacy.StatusCode
|
||
|
|
if status < 400 || status > 599 {
|
||
|
|
status = http.StatusForbidden
|
||
|
|
}
|
||
|
|
code := legacy.ErrorCode
|
||
|
|
if code == "" {
|
||
|
|
code = "content_policy_violation"
|
||
|
|
}
|
||
|
|
return Decision{
|
||
|
|
Kind: DecisionBlock, HTTPStatus: status, ErrorCode: code, ClientMessage: legacy.Message,
|
||
|
|
Legacy: legacy, Prompt: prompt, AllowNextStage: false,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if prompt == nil {
|
||
|
|
return allowDecision(legacy, nil)
|
||
|
|
}
|
||
|
|
switch prompt.Kind {
|
||
|
|
case DecisionBlock:
|
||
|
|
return Decision{Kind: DecisionBlock, HTTPStatus: http.StatusForbidden, ErrorCode: ErrorCodeBlocked,
|
||
|
|
ClientMessage: "提示词安全审计拒绝了该请求,请调整输入后重试", Legacy: legacy, Prompt: prompt}
|
||
|
|
case DecisionInvalid:
|
||
|
|
return Decision{Kind: DecisionInvalid, HTTPStatus: http.StatusServiceUnavailable, ErrorCode: ErrorCodeInvalidResponse,
|
||
|
|
ClientMessage: "提示词安全审计暂时不可用,请稍后重试", Legacy: legacy, Prompt: prompt}
|
||
|
|
case DecisionUnavailable:
|
||
|
|
return Decision{Kind: DecisionUnavailable, HTTPStatus: http.StatusServiceUnavailable, ErrorCode: ErrorCodeUnavailable,
|
||
|
|
ClientMessage: "提示词安全审计暂时不可用,请稍后重试", Legacy: legacy, Prompt: prompt}
|
||
|
|
case DecisionFlag:
|
||
|
|
return Decision{Kind: DecisionFlag, HTTPStatus: http.StatusOK, Legacy: legacy, Prompt: prompt, AllowNextStage: true}
|
||
|
|
default:
|
||
|
|
return allowDecision(legacy, prompt)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func allowDecision(legacy *LegacyDecision, prompt *PromptDecision) Decision {
|
||
|
|
return Decision{Kind: DecisionAllow, HTTPStatus: http.StatusOK, Legacy: legacy, Prompt: prompt, AllowNextStage: true}
|
||
|
|
}
|
||
|
|
|
||
|
|
func unavailablePromptDecision(code string) *PromptDecision {
|
||
|
|
kind := DecisionUnavailable
|
||
|
|
if code == ErrorCodeInvalidResponse {
|
||
|
|
kind = DecisionInvalid
|
||
|
|
}
|
||
|
|
return &PromptDecision{Kind: kind, ErrorCode: code, AllowNextStage: false}
|
||
|
|
}
|