Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
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}
|
|
}
|