Merge pull request 'Wire the EXT-11 scoped-host factory through the composition root' (#108) from feature/ext-11-scoped-host-factory into main
ci / vet-and-test (push) Successful in 39s
ci / vet-and-test (push) Successful in 39s
This commit was merged in pull request #108.
This commit is contained in:
@@ -378,6 +378,8 @@ func Run(opt Options) error {
|
||||
// hostcaps:hostapi 闭包与进程内插件句柄共用的 owner 作用域能力(EXT-1 批注 1)。
|
||||
authzSlot := &projectAuthz{}
|
||||
caps := hostcaps{st: st, pipe: pipe, set: setSvc, authz: authzSlot}
|
||||
// EXT-11:公开受限 Host 工厂(下游恢复路径经 extension.NewScopedHost 重建句柄)。
|
||||
wireScopedHostFactory(caps)
|
||||
applyCompiled(skillsRoot, bus, caps)
|
||||
mgr.OnUsage = func(owner string, tokens int) {
|
||||
budget := int(setSvc.Effective(owner, "").GetNumber("llm.dailyTokenBudget", 0))
|
||||
|
||||
@@ -51,6 +51,15 @@ func (a *projectAuthz) Authorize(owner string, projectID core.ID) (bool, error)
|
||||
return fn(owner, projectID)
|
||||
}
|
||||
|
||||
// wireScopedHostFactory EXT-11:把「按 owner+pluginID 构造受限 Host」注入公开面
|
||||
// (extension.SetScopedHostFactory)。组合根在 hostcaps 就绪后调用;重建句柄与
|
||||
// 分发期同构(owner 作用域、来源前缀强制、KV/任务台账隔离)。
|
||||
func wireScopedHostFactory(caps hostcaps) {
|
||||
extension.SetScopedHostFactory(func(owner, pluginID string) extension.Host {
|
||||
return inprocHost{caps: caps, pluginID: pluginID, owner: owner}
|
||||
})
|
||||
}
|
||||
|
||||
// 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})
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -21,6 +21,7 @@ HostClient
|
||||
InProc
|
||||
Main
|
||||
NewHostClient
|
||||
NewScopedHost
|
||||
Options
|
||||
RegisterCompiled
|
||||
Registry
|
||||
@@ -33,4 +34,5 @@ Request
|
||||
ResetCompiled
|
||||
ResourceAuthorizer
|
||||
SetRunner
|
||||
SetScopedHostFactory
|
||||
SurfaceVersion
|
||||
|
||||
Reference in New Issue
Block a user