Merge the EXT-20 shared regulations and web evidence extension point
governance / governance (push) Successful in 21s
ci / vet-and-test (push) Successful in 1m13s

Why: U9 base public contract for the regulations knowledge base.
Evidence: CI success on f3d2698d (after syncing main) (ci/vet-and-test, governance).
Impact: Additive optional capability; SurfaceVersion 2.15.0-experimental.
Refs: .task/feature-regulations-kb-ext20.md
This commit was merged in pull request #212.
This commit is contained in:
2026-10-18 09:58:40 +00:00
11 changed files with 1088 additions and 2 deletions
+30
View File
@@ -0,0 +1,30 @@
# feature/regulations-kb-ext20 任务卡
任务主记录:本文件随 PR 描述维护。
状态:已完成(合入前闭合);owner:weichen。PR:https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Backend/pulls/212
## 基线与授权
- 权威仓:ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-System-Backend.git;root_target:main;immediate_target:main
- branch_kind:standalone;跨仓总任务:Agent-System-Dev `feature/regulations-kb`([Dev PR #614](https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/614))
- R0(initial_main_sha):cac60c0;工作区:/home/weichen/projects/Agent-System-Backend-wt-feature-regulations-kb-ext20;分支:feature/regulations-kb-ext20
- 授权与交付终点:用户 2026-10-18 /goal 指示按法规知识库计划实现全部内容;本任务交付 U9 的基座公开契约 EXT-20(提案:Documents `docs/proposals/2026-10-18-ext-20-regulations-provider.md`,分支 `feature/regulations-kb-u9-contract`),经 PR 合入 main。不含法规提供方实现(属 KB 仓)与 RDM 消费(U6)。
- 实施执行:agent-per-unit(配置:AGENT-EXECUTION.md)
## 目标与非目标
- 目标:`pkg/extension` 新增 `RegulationsProvider`/`RegulationsRegistrar`/`RegulationsHost` 与 DTO;进程内注册槽仅接受插件 `kbreg` 且只注册一次;分发 Host 实现四个方法并强制调用方身份、查询最小化(机密词拒绝、长度限制)、状态透传与无明文审计;快照、边界与契约/兼容测试。
- 非目标:不实现搜索或语料检索;不改 `KnowledgeSearch` 语义。
## Agent 实施收据
| 单元 ID | 预期范围 | 角色 | 候选顺序 | attempt | CLI/模型 | 终态 | transport | fallback | 源码外收据路径 |
|---|---|---|---|---|---|---|---|---|---|
| backend-ext20-regulations-provider | pkg/extension、internal/app | implementation | cursor, antigravity, pi | 1 | cursor-agent-stable / auto(session not-observable,新进程) | success | refs/agent-exec/cfc2e055f212789112cf11716f5649c5/c04b36e76696 → aaed108(apply 快进) | 否 | /tmp/regkb-u9-receipt.json |
## 结果与验证(完成后填写)
- 结果:`pkg/extension/regulations.go`(EXT-20 接口、DTO、状态常量、`ErrRegulationsUnavailable`)、`surface.txt` 与 `SurfaceVersion` 2.15.0-experimental;`internal/app/regulations.go` 注册槽(仅 kbreg、一次、非 nil;声明式拒绝)与 inprocHost 四方法(有效账户检查、调用方取自分发上下文、schema 校验、查询去空白/1–256 字符/排除词拒绝且不调用提供方、状态透传、审计仅含修订/计数/状态/查询 sha256);单元与兼容测试。
- 验证(SELF,host 于 HEAD aaed108):`go vet ./pkg/extension/ ./internal/app/` 与 `go test ./pkg/extension/... ./internal/app/...` PASS;全量 `GOWORK=off go vet ./... && go test -count=1 ./...` 40 包 ok,仅 `pkg/identity TestDSNFileMode` 失败——在未改动基线 cac60c0 上同样失败(本机环境问题,本变更未触及 pkg/identity),以 CI(GATE)为准。
- Review:host 主模型审阅实际 diff(base 3187a00 / head aaed108),与提案第 2、4、5 节一致,无阻塞项。
- 影响与剩余项:新增可选能力,未注册提供方时返回不可用,既有行为不变。
+1 -1
View File
@@ -612,7 +612,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