agent-exec transport cfc2e055f212789112cf11716f5649c5/c04b36e76696

This commit is contained in:
agent-exec-transport
2026-10-18 09:38:04 +00:00
parent 3187a009f6
commit aaed108d79
10 changed files with 1058 additions and 2 deletions
+1 -1
View File
@@ -649,7 +649,7 @@ func Run(opt Options) error {
if err != nil { if err != nil {
return err return err
} }
caps := hostcaps{projects: projectSlot, directory: directorySlot, st: st, pipe: pipe, set: setSvc, authz: authzSlot, surfaces: surfaceSvc, allowImplicitLocal: !authOn, caps := hostcaps{projects: projectSlot, directory: directorySlot, regulations: &regulationsSlot{}, st: st, pipe: pipe, set: setSvc, authz: authzSlot, surfaces: surfaceSvc, allowImplicitLocal: !authOn,
accountGuard: func(owner string) error { accountGuard: func(owner string) error {
valid, err := effectiveAccount(context.Background(), owner) valid, err := effectiveAccount(context.Background(), owner)
if err != nil { if err != nil {
+19
View File
@@ -127,6 +127,17 @@ func (r pluginRegistry) RegisterAccountDirectoryScopeResolver(fn extension.Accou
return r.caps.directory.register(r.pluginID, fn) return r.caps.directory.register(r.pluginID, fn)
} }
func (r compiledRegistry) RegisterRegulationsProvider(extension.RegulationsProvider) error {
return errors.New("extension: declarative plugins cannot register a regulations provider")
}
func (r pluginRegistry) RegisterRegulationsProvider(p extension.RegulationsProvider) error {
if r.caps.regulations == nil {
return errors.New("extension: regulations provider registration unavailable")
}
return r.caps.regulations.register(r.pluginID, p)
}
// RegisterResourceAuthorizer 进程内扩展(EXT-1):填充 EXT-8 适配器槽 // RegisterResourceAuthorizer 进程内扩展(EXT-1):填充 EXT-8 适配器槽
// (hostcaps.authz 共享指针;首个注册者生效,组合根已装配进 Server/runmgr)。 // (hostcaps.authz 共享指针;首个注册者生效,组合根已装配进 Server/runmgr)。
// 此前 pluginRegistry 未覆盖该方法——进程内插件注册适配器会命中声明式分支的 // 此前 pluginRegistry 未覆盖该方法——进程内插件注册适配器会命中声明式分支的
@@ -169,6 +180,14 @@ func (r recordRegistry) RegisterAccountDirectoryScopeResolver(fn extension.Accou
return registrar.RegisterAccountDirectoryScopeResolver(fn) return registrar.RegisterAccountDirectoryScopeResolver(fn)
} }
func (r recordRegistry) RegisterRegulationsProvider(p extension.RegulationsProvider) error {
registrar, ok := r.Registry.(extension.RegulationsRegistrar)
if !ok {
return errors.New("extension: regulations provider registration unavailable")
}
return registrar.RegisterRegulationsProvider(p)
}
func (r recordRegistry) AddCommand(spec json.RawMessage) error { func (r recordRegistry) AddCommand(spec json.RawMessage) error {
if err := r.Registry.AddCommand(spec); err != nil { if err := r.Registry.AddCommand(spec); err != nil {
return err return err
+1
View File
@@ -23,6 +23,7 @@ import (
type hostcaps struct { type hostcaps struct {
projects *projectLifecycleSlot projects *projectLifecycleSlot
directory *accountDirectorySlot directory *accountDirectorySlot
regulations *regulationsSlot // EXT-20 法规提供方槽(共享指针;nil/未注册=不可用)
accountGuard func(string) error accountGuard func(string) error
allowImplicitLocal bool allowImplicitLocal bool
st *store.Store st *store.Store
+220
View File
@@ -0,0 +1,220 @@
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{}
+134
View File
@@ -0,0 +1,134 @@
package app
import (
"context"
"encoding/json"
"errors"
"path/filepath"
"testing"
"time"
agentcommand "agent-command"
"agent-system-backend/internal/cmdbus"
"agent-system-backend/internal/store"
"agent-system-backend/pkg/extension"
)
// compatRegulationsPlugin stands in for the external kbreg repository: it uses
// only pkg/extension types and registers through the optional Registrar.
type compatRegulationsPlugin struct{ seen []extension.RegulationsCaller }
func (p *compatRegulationsPlugin) Register(reg extension.Registry) error {
registrar, ok := reg.(extension.RegulationsRegistrar)
if !ok {
return errors.New("regulations registrar unavailable")
}
return registrar.RegisterRegulationsProvider(p)
}
func (p *compatRegulationsPlugin) SearchRegulations(_ context.Context, c extension.RegulationsCaller, req extension.RegulationsSearchRequest) (extension.RegulationsSearchResult, error) {
p.seen = append(p.seen, c)
return extension.RegulationsSearchResult{CorpusRevision: "rev-compat", State: extension.RegulationsStateOK,
Hits: []extension.RegulationHit{{RegulationID: "r", EditionID: "e", NodeID: "n", Disclosure: extension.RegulationDisclosureExcerpt}}}, nil
}
func (p *compatRegulationsPlugin) ResolveRegulationEvidence(_ context.Context, c extension.RegulationsCaller, req extension.RegulationEvidenceRequest) (extension.RegulationEvidence, error) {
p.seen = append(p.seen, c)
return extension.RegulationEvidence{CorpusRevision: req.CorpusRevision, NodeID: req.NodeID, Disclosure: extension.RegulationDisclosureFull, HashVerified: true}, nil
}
func (p *compatRegulationsPlugin) SearchWeb(_ context.Context, c extension.RegulationsCaller, req extension.WebSearchRequest) (extension.WebSearchResult, error) {
p.seen = append(p.seen, c)
return extension.WebSearchResult{Provider: "compat", SentQuery: req.Query, State: extension.WebSearchStateEmpty}, nil
}
func (p *compatRegulationsPlugin) CaptureWeb(_ context.Context, c extension.RegulationsCaller, req extension.WebCaptureRequest) (extension.WebCapture, error) {
p.seen = append(p.seen, c)
return extension.WebCapture{FinalURL: req.URL, State: extension.WebCaptureStateFailed, Reason: "offline"}, nil
}
// compatRegulationsConsumer is a second compiled plugin that reaches the
// provider only through the dispatch Host type assertion.
func compatRegulationsConsumer(reg extension.Registry) error {
return reg.AddCommandHandler(json.RawMessage(`{"id":"consumer.regs","title":"regs"}`),
func(ctx context.Context, h extension.Host, req extension.Request) (any, error) {
regs, ok := h.(extension.RegulationsHost)
if !ok {
return nil, errors.New("regulations host capability missing")
}
search, err := regs.SearchRegulations(ctx, extension.RegulationsSearchRequest{SchemaVersion: extension.RegulationsSchemaVersion, Query: "注册检验"})
if err != nil {
return nil, err
}
web, err := regs.SearchWeb(ctx, extension.WebSearchRequest{SchemaVersion: extension.RegulationsSchemaVersion, Query: "注册检验 指导原则"})
if err != nil {
return nil, err
}
return map[string]any{"revision": search.CorpusRevision, "webState": web.State}, nil
})
}
func newRegulationsCompatApp(t *testing.T, plugins ...extension.InProc) (*cmdbus.Bus, *store.Store, hostcaps) {
t.Helper()
extension.ResetCompiled()
t.Cleanup(extension.ResetCompiled)
for _, p := range plugins {
extension.RegisterCompiled(p)
}
st, err := store.OpenSQLite(filepath.Join(t.TempDir(), "compat.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { st.Close() })
if err := st.EnsureUser("alice", ""); err != nil {
t.Fatal(err)
}
bus := cmdbus.New(st, nil, func() time.Time { return time.Unix(1700000000, 0).UTC() })
caps := hostcaps{st: st, regulations: &regulationsSlot{}}
applyCompiled("", bus, caps)
return bus, st, caps
}
func TestRegulationsCompiledPluginCompatibility(t *testing.T) {
provider := &compatRegulationsPlugin{}
impostor := &compatRegulationsPlugin{}
bus, st, caps := newRegulationsCompatApp(t,
extension.InProc{ID: "impostor", Register: impostor.Register},
extension.InProc{ID: "kbreg", Register: provider.Register},
extension.InProc{ID: "consumer", Register: compatRegulationsConsumer},
)
if caps.regulations.get() != extension.RegulationsProvider(provider) {
t.Fatal("compiled kbreg provider was not the registered provider")
}
resp := bus.Dispatch(context.Background(), cmdbus.Request{ID: "consumer.regs", Owner: "alice", Actor: "alice", Entrance: "button"})
if resp.Receipt.State != agentcommand.StateCommitted {
t.Fatalf("dispatch = %+v", resp.Receipt)
}
payload, _ := resp.Payload.(map[string]any)
if payload["revision"] != "rev-compat" || payload["webState"] != extension.WebSearchStateEmpty {
t.Fatalf("payload = %+v", resp.Payload)
}
want := extension.RegulationsCaller{PluginID: "consumer", Actor: "alice", Owner: "alice"}
if len(provider.seen) != 2 || provider.seen[0] != want || provider.seen[1] != want || len(impostor.seen) != 0 {
t.Fatalf("provider callers = %+v impostor = %+v", provider.seen, impostor.seen)
}
evs, err := st.ListAuditEvents("alice", 20)
if err != nil {
t.Fatal(err)
}
actions := map[string]bool{}
for _, ev := range evs {
actions[ev.Action] = true
}
if !actions[auditRegulationsSearch] || !actions[auditWebSearch] {
t.Fatalf("host audit missing: %+v", evs)
}
}
func TestRegulationsCompiledConsumerWithoutProvider(t *testing.T) {
bus, _, _ := newRegulationsCompatApp(t, extension.InProc{ID: "consumer", Register: compatRegulationsConsumer})
resp := bus.Dispatch(context.Background(), cmdbus.Request{ID: "consumer.regs", Owner: "alice", Actor: "alice", Entrance: "button"})
if resp.Receipt.State == agentcommand.StateCommitted {
t.Fatalf("consumer succeeded without a regulations provider: %+v", resp.Payload)
}
}
+347
View File
@@ -0,0 +1,347 @@
package app
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"path/filepath"
"reflect"
"strings"
"testing"
"time"
"agent-system-backend/internal/store"
"agent-system-backend/pkg/extension"
)
type fakeRegulationsProvider struct {
callers []extension.RegulationsCaller
webQueries []string
search extension.RegulationsSearchResult
evidence extension.RegulationEvidence
web extension.WebSearchResult
capture extension.WebCapture
err error
}
func (f *fakeRegulationsProvider) SearchRegulations(_ context.Context, c extension.RegulationsCaller, _ extension.RegulationsSearchRequest) (extension.RegulationsSearchResult, error) {
f.callers = append(f.callers, c)
return f.search, f.err
}
func (f *fakeRegulationsProvider) ResolveRegulationEvidence(_ context.Context, c extension.RegulationsCaller, _ extension.RegulationEvidenceRequest) (extension.RegulationEvidence, error) {
f.callers = append(f.callers, c)
return f.evidence, f.err
}
func (f *fakeRegulationsProvider) SearchWeb(_ context.Context, c extension.RegulationsCaller, req extension.WebSearchRequest) (extension.WebSearchResult, error) {
f.callers = append(f.callers, c)
f.webQueries = append(f.webQueries, req.Query)
return f.web, f.err
}
func (f *fakeRegulationsProvider) CaptureWeb(_ context.Context, c extension.RegulationsCaller, _ extension.WebCaptureRequest) (extension.WebCapture, error) {
f.callers = append(f.callers, c)
return f.capture, f.err
}
func newRegulationsTestHost(t *testing.T) (inprocHost, *store.Store) {
t.Helper()
st, err := store.OpenSQLite(filepath.Join(t.TempDir(), "regulations.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { st.Close() })
if err := st.EnsureUser("alice", ""); err != nil {
t.Fatal(err)
}
caps := hostcaps{st: st, regulations: &regulationsSlot{}}
return inprocHost{caps: caps, pluginID: "rdm", owner: "alice", actor: "alice"}, st
}
func registerFakeRegulations(t *testing.T, h inprocHost) *fakeRegulationsProvider {
t.Helper()
p := &fakeRegulationsProvider{}
if err := (pluginRegistry{pluginID: "kbreg", caps: h.caps}).RegisterRegulationsProvider(p); err != nil {
t.Fatal(err)
}
return p
}
const regV = extension.RegulationsSchemaVersion
func callAllRegulations(h inprocHost, version int) []error {
ctx := context.Background()
_, e1 := h.SearchRegulations(ctx, extension.RegulationsSearchRequest{SchemaVersion: version, Query: "医疗器械 临床评价"})
_, e2 := h.ResolveRegulationEvidence(ctx, extension.RegulationEvidenceRequest{SchemaVersion: version, CorpusRevision: "rev-1", NodeID: "n1"})
_, e3 := h.SearchWeb(ctx, extension.WebSearchRequest{SchemaVersion: version, Query: "临床评价 指导原则"})
_, e4 := h.CaptureWeb(ctx, extension.WebCaptureRequest{SchemaVersion: version, URL: "https://www.nmpa.gov.cn/x", SearchQuery: "临床评价 指导原则"})
return []error{e1, e2, e3, e4}
}
func TestRegulationsUnavailableWithoutProvider(t *testing.T) {
h, st := newRegulationsTestHost(t)
for i, err := range callAllRegulations(h, regV) {
if !errors.Is(err, extension.ErrRegulationsUnavailable) {
t.Fatalf("method %d without provider: %v", i, err)
}
}
h.caps.regulations = nil
for i, err := range callAllRegulations(h, regV) {
if !errors.Is(err, extension.ErrRegulationsUnavailable) {
t.Fatalf("method %d without slot: %v", i, err)
}
}
if evs, err := st.ListAuditEvents("alice", 10); err != nil || len(evs) != 0 {
t.Fatalf("unavailable calls audited: %+v %v", evs, err)
}
}
func TestRegulationsRegistrationOnlyTrustedKbregOnce(t *testing.T) {
h, _ := newRegulationsTestHost(t)
first, second := &fakeRegulationsProvider{}, &fakeRegulationsProvider{}
if err := (pluginRegistry{pluginID: "rdm", caps: h.caps}).RegisterRegulationsProvider(first); err == nil {
t.Fatal("non-kbreg plugin registered a regulations provider")
}
kbreg := pluginRegistry{pluginID: "kbreg", caps: h.caps}
if err := kbreg.RegisterRegulationsProvider(nil); err == nil {
t.Fatal("nil provider registered")
}
if err := kbreg.RegisterRegulationsProvider(first); err != nil {
t.Fatal(err)
}
if err := kbreg.RegisterRegulationsProvider(second); err == nil {
t.Fatal("duplicate registration accepted")
}
if h.caps.regulations.get() != first {
t.Fatal("duplicate registration replaced the first provider")
}
if err := (pluginRegistry{pluginID: "kbreg"}).RegisterRegulationsProvider(first); err == nil {
t.Fatal("registration without a host slot accepted")
}
}
func TestRegulationsDeclarativeRegistrationRejected(t *testing.T) {
h, _ := newRegulationsTestHost(t)
p := &fakeRegulationsProvider{}
if err := (compiledRegistry{}).RegisterRegulationsProvider(p); err == nil {
t.Fatal("declarative registry accepted a regulations provider")
}
if err := (recordRegistry{Registry: pluginRegistry{pluginID: "kbreg", caps: h.caps}}).RegisterRegulationsProvider(p); err != nil {
t.Fatalf("record registry did not forward kbreg registration: %v", err)
}
if h.caps.regulations.get() != p {
t.Fatal("forwarded registration did not fill the slot")
}
}
func TestRegulationsCallerFilledFromDispatchContext(t *testing.T) {
h, _ := newRegulationsTestHost(t)
h.actor = "alice-session"
p := registerFakeRegulations(t, h)
p.web.State = extension.WebSearchStateOK
for i, err := range callAllRegulations(h, regV) {
if err != nil {
t.Fatalf("method %d: %v", i, err)
}
}
want := extension.RegulationsCaller{PluginID: "rdm", Actor: "alice-session", Owner: "alice"}
if len(p.callers) != 4 {
t.Fatalf("provider calls = %d", len(p.callers))
}
for i, got := range p.callers {
if got != want {
t.Fatalf("call %d caller = %+v, want %+v", i, got, want)
}
}
}
func TestRegulationsRejectsOtherSchemaVersions(t *testing.T) {
h, _ := newRegulationsTestHost(t)
p := registerFakeRegulations(t, h)
for _, version := range []int{0, regV + 1} {
for i, err := range callAllRegulations(h, version) {
if !errors.Is(err, errRegulationsSchema) {
t.Fatalf("schema %d method %d: %v", version, i, err)
}
}
}
if len(p.callers) != 0 {
t.Fatalf("provider reached with unsupported schema: %d calls", len(p.callers))
}
}
func TestRegulationsWebQueryBounds(t *testing.T) {
h, _ := newRegulationsTestHost(t)
p := registerFakeRegulations(t, h)
ctx := context.Background()
for _, q := range []string{"", " \t\n", strings.Repeat("法", maxWebQueryRunes+1)} {
if _, err := h.SearchWeb(ctx, extension.WebSearchRequest{SchemaVersion: regV, Query: q}); !errors.Is(err, errWebQueryInvalid) {
t.Fatalf("query of %d runes accepted: %v", len([]rune(q)), err)
}
}
if len(p.callers) != 0 {
t.Fatal("provider reached with an invalid query")
}
maxQuery := strings.Repeat("法", maxWebQueryRunes)
if _, err := h.SearchWeb(ctx, extension.WebSearchRequest{SchemaVersion: regV, Query: " " + maxQuery + " "}); err != nil {
t.Fatalf("256-rune query rejected: %v", err)
}
if len(p.webQueries) != 1 || p.webQueries[0] != maxQuery {
t.Fatalf("provider query was not trimmed: %q", p.webQueries)
}
}
func TestRegulationsExclusionRejectedBeforeProvider(t *testing.T) {
h, st := newRegulationsTestHost(t)
p := registerFakeRegulations(t, h)
ctx := context.Background()
query := "Project-Falcon 临床评价 指导原则"
res, err := h.SearchWeb(ctx, extension.WebSearchRequest{SchemaVersion: regV, Query: query,
Exclusions: []string{"", " ", "project-falcon"}})
if err != nil || res.State != extension.WebSearchStateRejected || res.SentQuery != "" || len(res.Results) != 0 {
t.Fatalf("excluded query result = %+v %v", res, err)
}
if len(p.callers) != 0 {
t.Fatal("provider received a query containing an excluded term")
}
evs, err := st.ListAuditEvents("alice", 10)
if err != nil || len(evs) != 1 || evs[0].Action != auditWebSearch {
t.Fatalf("rejection audit: %+v %v", evs, err)
}
if strings.Contains(strings.ToLower(evs[0].Evidence), "falcon") || !strings.Contains(evs[0].Evidence, `"state":"rejected"`) {
t.Fatalf("rejection audit evidence = %s", evs[0].Evidence)
}
if _, err := h.SearchWeb(ctx, extension.WebSearchRequest{SchemaVersion: regV, Query: query, Exclusions: []string{"", " "}}); err != nil {
t.Fatal(err)
}
if len(p.callers) != 1 {
t.Fatal("blank exclusions blocked an allowed query")
}
}
func TestRegulationsProviderStatesPassThroughUnchanged(t *testing.T) {
h, _ := newRegulationsTestHost(t)
p := registerFakeRegulations(t, h)
at := time.Date(2026, 10, 18, 9, 0, 0, 0, time.UTC)
p.search = extension.RegulationsSearchResult{CorpusRevision: "rev-7", State: extension.RegulationsStateDegraded,
DegradedReasons: []string{"vector_index_unavailable"},
Hits: []extension.RegulationHit{{RegulationID: "r1", EditionID: "e1", NodeID: "n1",
Disclosure: extension.RegulationDisclosureWithheld, WithheldReason: "rights"},
{RegulationID: "r2", EditionID: "e2", NodeID: "n2", Disclosure: extension.RegulationDisclosureMetadataOnly}}}
p.evidence = extension.RegulationEvidence{CorpusRevision: "rev-7", NodeID: "n1",
Disclosure: extension.RegulationDisclosureWithheld, WithheldReason: "rights", HashVerified: false}
p.web = extension.WebSearchResult{Provider: "fake", SentQuery: "q", SearchedAt: at,
State: extension.WebSearchStateRateLimited, Reason: "quota"}
p.capture = extension.WebCapture{CaptureID: "c1", FetchedAt: at, Retention: extension.WebRetentionMetadataOnly,
State: extension.WebCaptureStateRejected, Reason: "robots"}
ctx := context.Background()
search, err := h.SearchRegulations(ctx, extension.RegulationsSearchRequest{SchemaVersion: regV, Query: "q"})
if err != nil || !reflect.DeepEqual(search, p.search) {
t.Fatalf("search = %+v %v", search, err)
}
evidence, err := h.ResolveRegulationEvidence(ctx, extension.RegulationEvidenceRequest{SchemaVersion: regV, CorpusRevision: "rev-7", NodeID: "n1"})
if err != nil || !reflect.DeepEqual(evidence, p.evidence) {
t.Fatalf("evidence = %+v %v", evidence, err)
}
web, err := h.SearchWeb(ctx, extension.WebSearchRequest{SchemaVersion: regV, Query: "q"})
if err != nil || !reflect.DeepEqual(web, p.web) {
t.Fatalf("web = %+v %v", web, err)
}
p.web.State = extension.WebSearchStateRejected
web, err = h.SearchWeb(ctx, extension.WebSearchRequest{SchemaVersion: regV, Query: "q"})
if err != nil || web.State != extension.WebSearchStateRejected {
t.Fatalf("provider rejection rewritten: %+v %v", web, err)
}
capture, err := h.CaptureWeb(ctx, extension.WebCaptureRequest{SchemaVersion: regV, URL: "https://example.org"})
if err != nil || !reflect.DeepEqual(capture, p.capture) {
t.Fatalf("capture = %+v %v", capture, err)
}
p.err = errors.New("provider failure")
if _, err := h.SearchRegulations(ctx, extension.RegulationsSearchRequest{SchemaVersion: regV, Query: "q"}); !errors.Is(err, p.err) {
t.Fatalf("provider failure wrapped as success: %v", err)
}
}
func TestRegulationsAuditEvidenceOmitsQueryAndText(t *testing.T) {
h, st := newRegulationsTestHost(t)
p := registerFakeRegulations(t, h)
const (
secretQuery = "第十七条 secret-query-text"
secretExcerpt = "SECRET-EXCERPT 原文片段"
secretPage = "SECRET-PAGE 网页全文"
)
p.search = extension.RegulationsSearchResult{CorpusRevision: "rev-9", State: extension.RegulationsStateOK,
Hits: []extension.RegulationHit{{NodeID: "n1", Excerpt: secretExcerpt, Disclosure: extension.RegulationDisclosureExcerpt}}}
p.evidence = extension.RegulationEvidence{CorpusRevision: "rev-9", NodeID: "n1", Excerpt: secretExcerpt,
Disclosure: extension.RegulationDisclosureFull, HashVerified: true}
p.web = extension.WebSearchResult{SentQuery: secretQuery, State: extension.WebSearchStateOK,
Results: []extension.WebSearchHit{{Rank: 1, URL: "https://example.org", Snippet: secretExcerpt}}}
p.capture = extension.WebCapture{State: extension.WebCaptureStateCaptured, Retention: extension.WebRetentionFull,
Text: secretPage, Candidates: []extension.WebClauseCandidate{{Excerpt: secretExcerpt}}}
ctx := context.Background()
if _, err := h.SearchRegulations(ctx, extension.RegulationsSearchRequest{SchemaVersion: regV, Query: secretQuery}); err != nil {
t.Fatal(err)
}
if _, err := h.ResolveRegulationEvidence(ctx, extension.RegulationEvidenceRequest{SchemaVersion: regV, CorpusRevision: "rev-9", NodeID: "n1"}); err != nil {
t.Fatal(err)
}
if _, err := h.SearchWeb(ctx, extension.WebSearchRequest{SchemaVersion: regV, Query: " " + secretQuery + " "}); err != nil {
t.Fatal(err)
}
if _, err := h.CaptureWeb(ctx, extension.WebCaptureRequest{SchemaVersion: regV, URL: "https://example.org", SearchQuery: secretQuery}); err != nil {
t.Fatal(err)
}
evs, err := st.ListAuditEvents("alice", 10)
if err != nil || len(evs) != 4 {
t.Fatalf("audit events = %+v %v", evs, err)
}
sum := sha256.Sum256([]byte(secretQuery))
digest := hex.EncodeToString(sum[:])
want := map[string]regulationsAudit{
auditRegulationsSearch: {Revision: "rev-9", Count: 1, State: "ok", QuerySHA256: digest},
auditRegulationsEvidence: {Revision: "rev-9", Count: 1, State: "ok"},
auditWebSearch: {Count: 1, State: "ok", QuerySHA256: digest},
auditWebCapture: {Count: 1, State: "captured", QuerySHA256: digest},
}
for _, ev := range evs {
for _, secret := range []string{"secret-query-text", "第十七条", "SECRET-EXCERPT", "原文片段", "SECRET-PAGE", "网页全文"} {
if strings.Contains(ev.Evidence, secret) || strings.Contains(ev.Target, secret) {
t.Fatalf("%s audit leaks %q: %s", ev.Action, secret, ev.Evidence)
}
}
var fields map[string]any
if err := json.Unmarshal([]byte(ev.Evidence), &fields); err != nil {
t.Fatal(err)
}
for k := range fields {
if k != "revision" && k != "count" && k != "state" && k != "querySha256" {
t.Fatalf("%s audit has unexpected field %q", ev.Action, k)
}
}
var got regulationsAudit
if err := json.Unmarshal([]byte(ev.Evidence), &got); err != nil {
t.Fatal(err)
}
if w, ok := want[ev.Action]; !ok || got != w || ev.Owner != "alice" {
t.Fatalf("%s audit = %+v owner=%s, want %+v", ev.Action, got, ev.Owner, w)
}
delete(want, ev.Action)
}
}
func TestRegulationsRequireEffectiveAccount(t *testing.T) {
h, _ := newRegulationsTestHost(t)
p := registerFakeRegulations(t, h)
h.caps.accountGuard = func(string) error { return store.ErrAccountForbidden }
for i, err := range callAllRegulations(h, regV) {
if !errors.Is(err, store.ErrAccountForbidden) {
t.Fatalf("method %d ignored the account guard: %v", i, err)
}
}
if len(p.callers) != 0 {
t.Fatal("provider reached for an ineffective account")
}
}
+3 -1
View File
@@ -40,7 +40,9 @@ import (
// AccountDirectoryRegistrar for scoped, current account candidate decisions. // AccountDirectoryRegistrar for scoped, current account candidate decisions.
// 2.14.0-experimental:InProc.HostOptions / CLIHook,以及 MergeHostOptions、 // 2.14.0-experimental:InProc.HostOptions / CLIHook,以及 MergeHostOptions、
// RegisterCLIFlags、RunCLIHooks(组合二进制把插件声明并进基座启动,不覆盖显式参数)。 // RegisterCLIFlags、RunCLIHooks(组合二进制把插件声明并进基座启动,不覆盖显式参数)。
const SurfaceVersion = "2.14.0-experimental" // 2.15.0-experimental:EXT-20 可选 RegulationsHost 与受信进程内 RegulationsRegistrar
// (共享法规检索与网络证据;仅插件 kbreg 可注册提供方)。
const SurfaceVersion = "2.15.0-experimental"
// ErrNotLinked 表示进程没有空白导入 link 包,Main 无法启动服务。 // ErrNotLinked 表示进程没有空白导入 link 包,Main 无法启动服务。
var ErrNotLinked = errors.New("extension: runner not linked; blank-import agent-system-backend/pkg/extension/link") var ErrNotLinked = errors.New("extension: runner not linked; blank-import agent-system-backend/pkg/extension/link")
+209
View File
@@ -0,0 +1,209 @@
package extension
import (
"context"
"errors"
"time"
)
// RegulationsSchemaVersion is the only request schema the host accepts for
// EXT-20; any other value is rejected before reaching the provider.
const RegulationsSchemaVersion = 1
const (
RegulationsStateOK = "ok"
RegulationsStateEmpty = "empty"
RegulationsStateDegraded = "degraded"
RegulationsStateUnavailable = "unavailable"
RegulationDisclosureFull = "full"
RegulationDisclosureExcerpt = "excerpt"
RegulationDisclosureMetadataOnly = "metadata_only"
RegulationDisclosureWithheld = "withheld"
WebSearchStateOK = "ok"
WebSearchStateEmpty = "empty"
WebSearchStateUnavailable = "unavailable"
WebSearchStateRateLimited = "rate_limited"
WebSearchStateRejected = "rejected"
WebSearchStateError = "error"
WebCaptureStateCaptured = "captured"
WebCaptureStateRejected = "rejected"
WebCaptureStateFailed = "failed"
WebRetentionFull = "full"
WebRetentionExcerpt = "excerpt"
WebRetentionMetadataOnly = "metadata_only"
)
// ErrRegulationsUnavailable is returned by every RegulationsHost method when no
// trusted provider is registered. Callers must not fall back to KnowledgeSearch.
var ErrRegulationsUnavailable = errors.New("extension: regulations provider unavailable")
// RegulationsCaller is filled by the host from the trusted dispatch context.
// It deliberately has no project ID: shared regulation access neither widens
// nor narrows with a project.
type RegulationsCaller struct {
PluginID string `json:"pluginId"`
Actor string `json:"actor,omitempty"`
Owner string `json:"owner"`
}
// RegulationsSearchRequest searches the controlled regulation corpus. An empty
// CorpusRevision means the current published revision; the result echoes the
// revision actually searched.
type RegulationsSearchRequest struct {
SchemaVersion int `json:"schemaVersion"`
Query string `json:"query"`
CorpusRevision string `json:"corpusRevision,omitempty"`
Kinds []string `json:"kinds,omitempty"`
CurrentOnly bool `json:"currentOnly,omitempty"`
DocNo string `json:"docNo,omitempty"`
ClauseNo string `json:"clauseNo,omitempty"`
Limit int `json:"limit,omitempty"`
}
type RegulationsSearchResult struct {
CorpusRevision string `json:"corpusRevision"`
State string `json:"state"` // ok|empty|degraded|unavailable
DegradedReasons []string `json:"degradedReasons,omitempty"`
Hits []RegulationHit `json:"hits"`
}
type RegulationHit struct {
RegulationID string `json:"regulationId"`
EditionID string `json:"editionId"`
NodeID string `json:"nodeId"`
DocNo string `json:"docNo,omitempty"`
Title string `json:"title,omitempty"`
EditionLabel string `json:"editionLabel,omitempty"`
LegalStatus string `json:"legalStatus,omitempty"`
ClauseNo string `json:"clauseNo,omitempty"`
HeadingPath []string `json:"headingPath,omitempty"`
Excerpt string `json:"excerpt,omitempty"`
ExcerptStart int `json:"excerptStart,omitempty"`
ExcerptEnd int `json:"excerptEnd,omitempty"`
NodeSHA256 string `json:"nodeSha256,omitempty"`
SourceAssetSHA256 string `json:"sourceAssetSha256,omitempty"`
PageStart int `json:"pageStart,omitempty"`
PageEnd int `json:"pageEnd,omitempty"`
Disclosure string `json:"disclosure"` // full|excerpt|metadata_only|withheld
WithheldReason string `json:"withheldReason,omitempty"`
IsSummary bool `json:"isSummary,omitempty"`
MatchKinds []string `json:"matchKinds,omitempty"`
Score float64 `json:"score,omitempty"`
}
// RegulationEvidenceRequest retrieves the original text span of a node pinned
// to a corpus revision.
type RegulationEvidenceRequest struct {
SchemaVersion int `json:"schemaVersion"`
CorpusRevision string `json:"corpusRevision"`
NodeID string `json:"nodeId"`
Start int `json:"start,omitempty"`
End int `json:"end,omitempty"`
}
type RegulationEvidence struct {
CorpusRevision string `json:"corpusRevision"`
RegulationID string `json:"regulationId,omitempty"`
EditionID string `json:"editionId,omitempty"`
NodeID string `json:"nodeId"`
ClauseNo string `json:"clauseNo,omitempty"`
Excerpt string `json:"excerpt,omitempty"`
Start int `json:"start,omitempty"`
End int `json:"end,omitempty"`
NodeSHA256 string `json:"nodeSha256,omitempty"`
SourceAssetSHA256 string `json:"sourceAssetSha256,omitempty"`
PageStart int `json:"pageStart,omitempty"`
PageEnd int `json:"pageEnd,omitempty"`
Disclosure string `json:"disclosure"`
WithheldReason string `json:"withheldReason,omitempty"`
HashVerified bool `json:"hashVerified"`
}
// WebSearchRequest performs a real web search. Exclusions are caller-declared
// confidential terms (project names, numbers, people); a query containing any
// of them is rejected by the host before the provider sees it.
type WebSearchRequest struct {
SchemaVersion int `json:"schemaVersion"`
Query string `json:"query"`
Exclusions []string `json:"exclusions,omitempty"`
PreferOfficial bool `json:"preferOfficial,omitempty"`
Limit int `json:"limit,omitempty"`
}
type WebSearchResult struct {
Provider string `json:"provider,omitempty"`
SentQuery string `json:"sentQuery,omitempty"`
SearchedAt time.Time `json:"searchedAt"`
State string `json:"state"` // ok|empty|unavailable|rate_limited|rejected|error
Reason string `json:"reason,omitempty"`
Results []WebSearchHit `json:"results,omitempty"`
}
type WebSearchHit struct {
Rank int `json:"rank"`
URL string `json:"url"`
Title string `json:"title,omitempty"`
Snippet string `json:"snippet,omitempty"`
Publisher string `json:"publisher,omitempty"`
Official bool `json:"official,omitempty"`
}
// WebCaptureRequest fetches a page found by a prior SearchWeb call.
type WebCaptureRequest struct {
SchemaVersion int `json:"schemaVersion"`
URL string `json:"url"`
SearchProvider string `json:"searchProvider,omitempty"`
SearchQuery string `json:"searchQuery,omitempty"`
SearchRank int `json:"searchRank,omitempty"`
}
type WebCapture struct {
CaptureID string `json:"captureId,omitempty"`
FinalURL string `json:"finalUrl,omitempty"`
FetchedAt time.Time `json:"fetchedAt"`
ContentType string `json:"contentType,omitempty"`
ByteSize int64 `json:"byteSize,omitempty"`
SHA256 string `json:"sha256,omitempty"`
Retention string `json:"retention,omitempty"` // full|excerpt|metadata_only
// Text is present only when the source rights allow full retention.
Text string `json:"text,omitempty"`
Candidates []WebClauseCandidate `json:"candidates,omitempty"`
State string `json:"state"` // captured|rejected|failed
Reason string `json:"reason,omitempty"`
}
type WebClauseCandidate struct {
ClauseNo string `json:"clauseNo,omitempty"`
Excerpt string `json:"excerpt"`
Start int `json:"start"`
End int `json:"end"`
}
// RegulationsProvider is implemented and registered by the trusted regulations
// knowledge plugin (plugin ID "kbreg"); the host ships no implementation.
type RegulationsProvider interface {
SearchRegulations(context.Context, RegulationsCaller, RegulationsSearchRequest) (RegulationsSearchResult, error)
ResolveRegulationEvidence(context.Context, RegulationsCaller, RegulationEvidenceRequest) (RegulationEvidence, error)
SearchWeb(context.Context, RegulationsCaller, WebSearchRequest) (WebSearchResult, error)
CaptureWeb(context.Context, RegulationsCaller, WebCaptureRequest) (WebCapture, error)
}
// RegulationsRegistrar is optional on a compiled in-process Registry. Only
// plugin ID "kbreg" may register, and only once.
type RegulationsRegistrar interface {
RegisterRegulationsProvider(RegulationsProvider) error
}
// RegulationsHost is optional on dispatch Host handles (obtained by type
// assertion). A missing capability means unavailable; callers must not fall
// back to KnowledgeSearch.
type RegulationsHost interface {
SearchRegulations(context.Context, RegulationsSearchRequest) (RegulationsSearchResult, error)
ResolveRegulationEvidence(context.Context, RegulationEvidenceRequest) (RegulationEvidence, error)
SearchWeb(context.Context, WebSearchRequest) (WebSearchResult, error)
CaptureWeb(context.Context, WebCaptureRequest) (WebCapture, error)
}
+78
View File
@@ -0,0 +1,78 @@
package extension_test
import (
"context"
"encoding/json"
"strings"
"testing"
"agent-system-backend/pkg/extension"
)
// publicRegulationsProvider is a downstream provider built only from the
// public extension surface.
type publicRegulationsProvider struct{}
func (publicRegulationsProvider) SearchRegulations(context.Context, extension.RegulationsCaller, extension.RegulationsSearchRequest) (extension.RegulationsSearchResult, error) {
return extension.RegulationsSearchResult{State: extension.RegulationsStateEmpty}, nil
}
func (publicRegulationsProvider) ResolveRegulationEvidence(context.Context, extension.RegulationsCaller, extension.RegulationEvidenceRequest) (extension.RegulationEvidence, error) {
return extension.RegulationEvidence{Disclosure: extension.RegulationDisclosureMetadataOnly}, nil
}
func (publicRegulationsProvider) SearchWeb(context.Context, extension.RegulationsCaller, extension.WebSearchRequest) (extension.WebSearchResult, error) {
return extension.WebSearchResult{State: extension.WebSearchStateUnavailable}, nil
}
func (publicRegulationsProvider) CaptureWeb(context.Context, extension.RegulationsCaller, extension.WebCaptureRequest) (extension.WebCapture, error) {
return extension.WebCapture{State: extension.WebCaptureStateFailed}, nil
}
var _ extension.RegulationsProvider = publicRegulationsProvider{}
func TestRegulationsContractIsOptional(t *testing.T) {
var host extension.Host
if _, ok := host.(extension.RegulationsHost); ok {
t.Fatal("nil host must not expose regulations")
}
var registry extension.Registry
if _, ok := registry.(extension.RegulationsRegistrar); ok {
t.Fatal("nil registry must not expose regulations registration")
}
if extension.RegulationsSchemaVersion != 1 {
t.Fatalf("schema version = %d", extension.RegulationsSchemaVersion)
}
if extension.ErrRegulationsUnavailable == nil {
t.Fatal("unavailable error must be defined")
}
}
func TestRegulationsWireNames(t *testing.T) {
states := []string{extension.RegulationsStateOK, extension.RegulationsStateEmpty, extension.RegulationsStateDegraded,
extension.RegulationsStateUnavailable, extension.RegulationDisclosureFull, extension.RegulationDisclosureExcerpt,
extension.RegulationDisclosureMetadataOnly, extension.RegulationDisclosureWithheld, extension.WebSearchStateOK,
extension.WebSearchStateEmpty, extension.WebSearchStateUnavailable, extension.WebSearchStateRateLimited,
extension.WebSearchStateRejected, extension.WebSearchStateError, extension.WebCaptureStateCaptured,
extension.WebCaptureStateRejected, extension.WebCaptureStateFailed, extension.WebRetentionFull,
extension.WebRetentionExcerpt, extension.WebRetentionMetadataOnly}
want := "ok empty degraded unavailable full excerpt metadata_only withheld ok empty unavailable rate_limited " +
"rejected error captured rejected failed full excerpt metadata_only"
if got := strings.Join(states, " "); got != want {
t.Fatalf("wire states = %q", got)
}
raw, err := json.Marshal(extension.WebSearchRequest{SchemaVersion: extension.RegulationsSchemaVersion, Query: "q"})
if err != nil {
t.Fatal(err)
}
if string(raw) != `{"schemaVersion":1,"query":"q"}` {
t.Fatalf("optional fields not omitted: %s", raw)
}
raw, err = json.Marshal(extension.RegulationsCaller{PluginID: "rdm", Owner: "alice"})
if err != nil {
t.Fatal(err)
}
if strings.Contains(strings.ToLower(string(raw)), "project") {
t.Fatalf("caller must not carry a project: %s", raw)
}
}
+46
View File
@@ -30,6 +30,7 @@ Compiled
DelegatedProjectProjectionHost DelegatedProjectProjectionHost
DelegatedProjectProjectionHost.ProjectProjectionForCreator DelegatedProjectProjectionHost.ProjectProjectionForCreator
ErrNotLinked ErrNotLinked
ErrRegulationsUnavailable
Handler Handler
Host Host
Host.ArtifactContent Host.ArtifactContent
@@ -78,6 +79,33 @@ Registry.AddSkill
Registry.Note Registry.Note
Registry.RegisterResourceAuthorizer Registry.RegisterResourceAuthorizer
Registry.RegisterResourceAuthorizerV2 Registry.RegisterResourceAuthorizerV2
RegulationDisclosureExcerpt
RegulationDisclosureFull
RegulationDisclosureMetadataOnly
RegulationDisclosureWithheld
RegulationEvidence
RegulationEvidenceRequest
RegulationHit
RegulationsCaller
RegulationsHost
RegulationsHost.CaptureWeb
RegulationsHost.ResolveRegulationEvidence
RegulationsHost.SearchRegulations
RegulationsHost.SearchWeb
RegulationsProvider
RegulationsProvider.CaptureWeb
RegulationsProvider.ResolveRegulationEvidence
RegulationsProvider.SearchRegulations
RegulationsProvider.SearchWeb
RegulationsRegistrar
RegulationsRegistrar.RegisterRegulationsProvider
RegulationsSchemaVersion
RegulationsSearchRequest
RegulationsSearchResult
RegulationsStateDegraded
RegulationsStateEmpty
RegulationsStateOK
RegulationsStateUnavailable
Request Request
ResetCompiled ResetCompiled
ResourceAuthorizer ResourceAuthorizer
@@ -93,3 +121,21 @@ SurfaceHost.SurfaceList
SurfaceHost.SurfaceSave SurfaceHost.SurfaceSave
SurfaceSaveRequest SurfaceSaveRequest
SurfaceVersion SurfaceVersion
WebCapture
WebCaptureRequest
WebCaptureStateCaptured
WebCaptureStateFailed
WebCaptureStateRejected
WebClauseCandidate
WebRetentionExcerpt
WebRetentionFull
WebRetentionMetadataOnly
WebSearchHit
WebSearchRequest
WebSearchResult
WebSearchStateEmpty
WebSearchStateError
WebSearchStateOK
WebSearchStateRateLimited
WebSearchStateRejected
WebSearchStateUnavailable