Add the EXT-11 scoped-host surface with a red factory test

Why: EXT-11 is accepted; the surface (SetScopedHostFactory/NewScopedHost, SurfaceVersion 2.4.0-experimental) must land with the behavioral red pinned - wiring absent, NewScopedHost fails closed with ErrNotLinked.
Evidence: TestScopedHostFactory red at the wired-factory assertion; row follows on the task card.
Impact: pkg/extension additive surface (surface.txt +2 symbols); internal wireScopedHostFactory stub. Behavior unchanged.
Refs: EXT-11 proposal; task card.
This commit is contained in:
2026-10-08 17:00:53 +00:00
parent 8a9a354649
commit 182d8b5143
4 changed files with 97 additions and 1 deletions
+4
View File
@@ -51,6 +51,10 @@ func (a *projectAuthz) Authorize(owner string, projectID core.ID) (bool, error)
return fn(owner, projectID)
}
// wireScopedHostFactory EXT-11:把「按 owner+pluginID 构造受限 Host」注入公开面。
//(红占位:本提交不真正注入,实现在后续提交落地。)
func wireScopedHostFactory(caps hostcaps) {}
// search owner 作用域混合检索;projectID/topK 为空/0 时用管线默认值。
func (c hostcaps) search(ctx context.Context, owner string, projectID core.ID, text string, topK int) ([]core.RetrievalHit, error) {
return c.pipe.Retrieve(ctx, core.ID(owner), core.RetrievalQuery{ProjectID: projectID, Text: text, TopK: topK})
+57
View File
@@ -0,0 +1,57 @@
package app
import (
"context"
"path/filepath"
"testing"
"agent-system-backend/internal/rag"
"agent-system-backend/internal/settings"
"agent-system-backend/internal/store"
"agent-system-backend/pkg/extension"
)
// EXT-11 红测:注入前 NewScopedHost fail-closed;注入后重建句柄与分发期同构
//(owner 作用域、PluginID、KV 隔离、知识删除前缀强制)。
func TestScopedHostFactory(t *testing.T) {
// 注入前:fail-closed。
if _, err := extension.NewScopedHost("alice", "rdm"); err == nil {
t.Fatal("before wiring, NewScopedHost must fail closed(工厂未注入)")
}
st, err := store.OpenSQLite(filepath.Join(t.TempDir(), "t.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { st.Close() })
set := settings.NewService(st)
pipe := rag.NewPipeline(st, set, nil, nil)
caps := hostcaps{st: st, pipe: pipe, set: set}
// 组合根同款注入(app.Run 内以 wireScopedHostFactory 接线;此处直接调内部函数)。
wireScopedHostFactory(caps)
t.Cleanup(func() { extension.SetScopedHostFactory(nil) })
h, err := extension.NewScopedHost("alice", "rdm")
if err != nil {
t.Fatalf("NewScopedHost: %v", err)
}
if h.Owner() != "alice" || h.PluginID() != "rdm" {
t.Fatalf("Owner/PluginID = %s/%s, want alice/rdm", h.Owner(), h.PluginID())
}
// KV 往返落 owner 作用域。
if err := h.KVPut("k1", "v1"); err != nil {
t.Fatal(err)
}
if v, ok, err := h.KVGet("k1"); err != nil || !ok || v != "v1" {
t.Fatalf("KVGet = %q ok=%v err=%v", v, ok, err)
}
// 同构性:知识删除前缀强制(manual 不可及)。
if err := h.KnowledgeDelete(context.Background(), "manual"); err == nil {
t.Fatal("scoped host must keep the plugin:<id> prefix enforcement")
}
// 空 owner/pluginID:拒绝。
if _, err := extension.NewScopedHost("", "rdm"); err == nil {
t.Fatal("empty owner must be rejected")
}
}
+34 -1
View File
@@ -16,7 +16,9 @@ import (
// Options.DisableUploadIndexing。
// 2.3.0-experimental:EXT-12 最小开关 Options.DisableConvCardMirror(会话卡不镜像
// 进项目公共流)。
const SurfaceVersion = "2.3.0-experimental"
// 2.4.0-experimental:EXT-11 新增 SetScopedHostFactory/NewScopedHost(按 owner+pluginID
// 重建受限宿主句柄,供下游恢复路径)。
const SurfaceVersion = "2.4.0-experimental"
// ErrNotLinked 表示进程没有空白导入 link 包,Main 无法启动服务。
var ErrNotLinked = errors.New("extension: runner not linked; blank-import agent-system-backend/pkg/extension/link")
@@ -99,6 +101,37 @@ func SetRunner(fn func(Options) error) {
mu.Unlock()
}
// scopedHostFactory 由组合根在装配 hostcaps 后注入(EXT-11):按 owner+pluginID
// 构造与分发期同构的受限 Host。下游不要调用 SetScopedHostFactory。
var scopedHostFactory func(owner, pluginID string) Host
// SetScopedHostFactory 组合根注入(EXT-11)。下游不要调用。
func SetScopedHostFactory(fn func(owner, pluginID string) Host) {
mu.Lock()
scopedHostFactory = fn
mu.Unlock()
}
// NewScopedHost 按 owner+pluginID 重建受限宿主句柄(EXT-11):供下游任务恢复等
// 非分发期路径获取合法句柄;能力与分发期同构(owner 作用域、来源前缀强制、
// KV/任务台账隔离)。未 link 或未注入时返回错误(fail-closed,不降级)。
func NewScopedHost(owner, pluginID string) (Host, error) {
mu.Lock()
fn := scopedHostFactory
mu.Unlock()
if fn == nil {
return nil, ErrNotLinked
}
if owner == "" || pluginID == "" {
return nil, errors.New("extension: scoped host requires owner and pluginID")
}
h := fn(owner, pluginID)
if h == nil {
return nil, errors.New("extension: scoped host factory returned nil")
}
return h, nil
}
// Main 启动带有已登记扩展的服务。
func Main(opt Options) error {
mu.Lock()
+2
View File
@@ -21,6 +21,7 @@ HostClient
InProc
Main
NewHostClient
NewScopedHost
Options
RegisterCompiled
Registry
@@ -33,4 +34,5 @@ Request
ResetCompiled
ResourceAuthorizer
SetRunner
SetScopedHostFactory
SurfaceVersion