221 lines
7.3 KiB
Go
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{}
|