Files
Agent-System-Backend/internal/app/regulations.go
T

221 lines
7.3 KiB
Go

package app
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"strings"
"sync"
"unicode/utf8"
"agent-system-backend/pkg/extension"
)
// EXT-20:共享法规检索与网络证据。提供方由受信插件 kbreg 进程内注册;基座只
// 填写调用方身份、做查询最小化校验并审计,不改写提供方返回的状态。
const (
regulationsPluginID = "kbreg"
maxWebQueryRunes = 256
auditRegulationsSearch = "regulations.search"
auditRegulationsEvidence = "regulations.evidence"
auditWebSearch = "web.search"
auditWebCapture = "web.capture"
regulationsAuditInvalid = "invalid"
regulationsAuditError = "error"
regulationsAuditOK = "ok"
)
var (
errRegulationsSchema = errors.New("extension: unsupported regulations schema version")
errWebQueryInvalid = errors.New("extension: web search query must be 1-256 characters")
)
type regulationsSlot struct {
mu sync.RWMutex
provider extension.RegulationsProvider
}
func (s *regulationsSlot) register(pluginID string, p extension.RegulationsProvider) error {
if p == nil || pluginID != regulationsPluginID {
return errors.New("extension: regulations provider requires trusted kbreg registration")
}
s.mu.Lock()
defer s.mu.Unlock()
if s.provider != nil {
return errors.New("extension: regulations provider already registered")
}
s.provider = p
return nil
}
func (s *regulationsSlot) get() extension.RegulationsProvider {
if s == nil {
return nil
}
s.mu.RLock()
defer s.mu.RUnlock()
return s.provider
}
// regulationsAudit is the only content written to the audit trail: never the
// query text, excerpts or captured page text.
type regulationsAudit struct {
Revision string `json:"revision,omitempty"`
Count int `json:"count"`
State string `json:"state"`
QuerySHA256 string `json:"querySha256,omitempty"`
}
func regulationsQueryDigest(query string) string {
if query == "" {
return ""
}
sum := sha256.Sum256([]byte(query))
return hex.EncodeToString(sum[:])
}
// regulations resolves the provider and the trusted caller. Without an audit
// store the capability is unavailable: calls must never run unaudited.
func (h inprocHost) regulations() (extension.RegulationsProvider, extension.RegulationsCaller, error) {
if err := h.requireEffective(); err != nil {
return nil, extension.RegulationsCaller{}, err
}
p := h.caps.regulations.get()
if p == nil || h.caps.st == nil {
return nil, extension.RegulationsCaller{}, extension.ErrRegulationsUnavailable
}
return p, extension.RegulationsCaller{PluginID: h.pluginID, Actor: h.actor, Owner: h.owner}, nil
}
// auditRegulations appends one event per call. A call error takes precedence;
// otherwise an audit failure withholds the result.
func (h inprocHost) auditRegulations(action string, rec regulationsAudit, callErr error) error {
if callErr != nil && rec.State == "" {
rec.State = regulationsAuditError
}
evidence, err := json.Marshal(rec)
if err == nil {
err = h.Audit(action, regulationsPluginID, string(evidence))
}
if callErr != nil {
return callErr
}
return err
}
func (h inprocHost) SearchRegulations(ctx context.Context, req extension.RegulationsSearchRequest) (extension.RegulationsSearchResult, error) {
p, caller, err := h.regulations()
if err != nil {
return extension.RegulationsSearchResult{}, err
}
rec := regulationsAudit{Revision: req.CorpusRevision, QuerySHA256: regulationsQueryDigest(req.Query)}
if req.SchemaVersion != extension.RegulationsSchemaVersion {
rec.State = regulationsAuditInvalid
return extension.RegulationsSearchResult{}, h.auditRegulations(auditRegulationsSearch, rec, errRegulationsSchema)
}
res, err := p.SearchRegulations(ctx, caller, req)
if err == nil {
rec.Revision, rec.Count, rec.State = res.CorpusRevision, len(res.Hits), res.State
}
if err := h.auditRegulations(auditRegulationsSearch, rec, err); err != nil {
return extension.RegulationsSearchResult{}, err
}
return res, nil
}
func (h inprocHost) ResolveRegulationEvidence(ctx context.Context, req extension.RegulationEvidenceRequest) (extension.RegulationEvidence, error) {
p, caller, err := h.regulations()
if err != nil {
return extension.RegulationEvidence{}, err
}
rec := regulationsAudit{Revision: req.CorpusRevision}
if req.SchemaVersion != extension.RegulationsSchemaVersion {
rec.State = regulationsAuditInvalid
return extension.RegulationEvidence{}, h.auditRegulations(auditRegulationsEvidence, rec, errRegulationsSchema)
}
ev, err := p.ResolveRegulationEvidence(ctx, caller, req)
if err == nil {
rec.Count, rec.State = 1, regulationsAuditOK
if ev.CorpusRevision != "" {
rec.Revision = ev.CorpusRevision
}
}
if err := h.auditRegulations(auditRegulationsEvidence, rec, err); err != nil {
return extension.RegulationEvidence{}, err
}
return ev, nil
}
// webQueryExcluded reports whether query contains any non-empty exclusion
// term, case-insensitively.
func webQueryExcluded(query string, exclusions []string) bool {
lower := strings.ToLower(query)
for _, term := range exclusions {
term = strings.ToLower(strings.TrimSpace(term))
if term != "" && strings.Contains(lower, term) {
return true
}
}
return false
}
func (h inprocHost) SearchWeb(ctx context.Context, req extension.WebSearchRequest) (extension.WebSearchResult, error) {
p, caller, err := h.regulations()
if err != nil {
return extension.WebSearchResult{}, err
}
req.Query = strings.TrimSpace(req.Query)
rec := regulationsAudit{QuerySHA256: regulationsQueryDigest(req.Query)}
if req.SchemaVersion != extension.RegulationsSchemaVersion {
rec.State = regulationsAuditInvalid
return extension.WebSearchResult{}, h.auditRegulations(auditWebSearch, rec, errRegulationsSchema)
}
if req.Query == "" || !utf8.ValidString(req.Query) || utf8.RuneCountInString(req.Query) > maxWebQueryRunes {
rec.State = regulationsAuditInvalid
return extension.WebSearchResult{}, h.auditRegulations(auditWebSearch, rec, errWebQueryInvalid)
}
if webQueryExcluded(req.Query, req.Exclusions) {
res := extension.WebSearchResult{State: extension.WebSearchStateRejected, Reason: "query contains an excluded term"}
rec.State = res.State
if err := h.auditRegulations(auditWebSearch, rec, nil); err != nil {
return extension.WebSearchResult{}, err
}
return res, nil
}
res, err := p.SearchWeb(ctx, caller, req)
if err == nil {
rec.Count, rec.State = len(res.Results), res.State
}
if err := h.auditRegulations(auditWebSearch, rec, err); err != nil {
return extension.WebSearchResult{}, err
}
return res, nil
}
func (h inprocHost) CaptureWeb(ctx context.Context, req extension.WebCaptureRequest) (extension.WebCapture, error) {
p, caller, err := h.regulations()
if err != nil {
return extension.WebCapture{}, err
}
rec := regulationsAudit{QuerySHA256: regulationsQueryDigest(strings.TrimSpace(req.SearchQuery))}
if req.SchemaVersion != extension.RegulationsSchemaVersion {
rec.State = regulationsAuditInvalid
return extension.WebCapture{}, h.auditRegulations(auditWebCapture, rec, errRegulationsSchema)
}
capture, err := p.CaptureWeb(ctx, caller, req)
if err == nil {
rec.Count, rec.State = len(capture.Candidates), capture.State
}
if err := h.auditRegulations(auditWebCapture, rec, err); err != nil {
return extension.WebCapture{}, err
}
return capture, nil
}
var _ extension.RegulationsHost = inprocHost{}