Files
Agent-System-Backend/internal/app/hostcaps.go
T
agent-exec-transport e942a35b73
governance / governance (pull_request) Successful in 14s
ci / vet-and-test (pull_request) Successful in 1m18s
agent-exec transport 8315c523cef26780543b9b2130f4f4a4/5c8333ec4e92
2026-10-19 12:21:32 +00:00

958 lines
35 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package app
import (
"agent-system-backend/pkg/extension"
"context"
"fmt"
"sort"
"strconv"
"strings"
"sync"
"time"
authzsvc "agent-system-backend/internal/authz"
"agent-system-backend/internal/authz/catalog"
"agent-system-backend/internal/cmdbus"
"agent-system-backend/internal/ingest"
"agent-system-backend/internal/rag"
"agent-system-backend/internal/runtime"
"agent-system-backend/internal/settings"
"agent-system-backend/internal/store"
"agent-system-backend/internal/surfacesvc"
core "agent-system-core"
)
// hostcaps 聚合 owner 作用域的宿主能力(存储、检索管线、LLM 路由)。
// 同一实现同时服务 hostapi.Server 的 Search/Complete 闭包与进程内
// inprocHost(EXT-1 批注 1:复用现有 owner 作用域函数,不新造句柄)。
type hostcaps struct {
projects *projectLifecycleSlot
workObserver *projectWorkObserverSlot
directory *accountDirectorySlot
contexts *projectContextSlot
projectAudit *projectAuditSlot
regulations *regulationsSlot // EXT-20 法规提供方槽(共享指针;nil/未注册=不可用)
accountGuard func(string) error
allowImplicitLocal bool
st *store.Store
pipe *rag.Pipeline
set *settings.Service
authz *projectAuthz // EXT-8 适配器槽(共享指针;首个注册者生效)
surfaces *surfacesvc.Service // EXT-17 Surface 能力槽(nil = 未装配,明确拒绝)
now func() time.Time
}
// projectAuthz EXT-8 资源授权适配器槽:进程内扩展注册,组合根装配进 Server。
type projectAuthz struct {
mu sync.Mutex
holder string // 持有者 pluginID(首个注册者生效)
fn extension.ResourceAuthorizer
holder2 string // EXT-8b 资源级持有者(首个注册者生效)
fn2 extension.ResourceAuthorizerV2
enrich func(extension.ResourceAuthzQuery) (extension.ResourceAuthzQuery, error)
}
func (a *projectAuthz) set(pluginID string, fn extension.ResourceAuthorizer) {
a.mu.Lock()
defer a.mu.Unlock()
if a.fn != nil {
return // 首个注册者生效;后来者忽略(调用方经日志可查)
}
a.holder, a.fn = pluginID, fn
}
func (a *projectAuthz) setV2(pluginID string, fn extension.ResourceAuthorizerV2) {
a.mu.Lock()
defer a.mu.Unlock()
if a.fn2 != nil {
return
}
a.holder2, a.fn2 = pluginID, fn
}
// AuthorizeResource EXT-8b 资源级判定(U9):V2 注册时按查询六要素咨询(含
// owner 自有命中与共享读取,fail-closed);未注册回落 V1 的 owner×project
// 布尔(兼容现行行为)。查询字段由执行端受信构造。
func (a *projectAuthz) AuthorizeResource(q extension.ResourceAuthzQuery) (bool, error) {
a.mu.Lock()
fn2, fn1, enrich := a.fn2, a.fn, a.enrich
a.mu.Unlock()
if fn2 != nil {
if q.Actor == "" || q.Action == "" || q.ResourceType == "" || q.ResourceID == "" {
return false, nil // 受信上下文字段缺失=不可判定=拒绝
}
if enrich != nil {
var err error
q, err = enrich(q)
if err != nil {
return false, err
}
}
return fn2(q)
}
if fn1 == nil || q.Owner == "" || q.ProjectID == "" {
return false, nil
}
return fn1(q.Actor, q.ProjectID)
}
// Authorize owner×project 共享读取判定;未注册一律 false(默认拒绝)。
func (a *projectAuthz) hasV2() bool {
a.mu.Lock()
defer a.mu.Unlock()
return a.fn2 != nil
}
func (a *projectAuthz) Authorize(owner string, projectID core.ID) (bool, error) {
a.mu.Lock()
fn := a.fn
a.mu.Unlock()
if fn == nil || owner == "" || projectID == "" {
return false, nil
}
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 时用管线默认值。
// sharedRetriever EXT-13:rag.Pipeline.RetrieveShared 形状(避免 app 直接 import rag
// 的具体类型断言散落)。
type sharedRetriever interface {
RetrieveShared(ctx context.Context, owner core.ID, q core.RetrievalQuery, allow func(docOwner, projectID core.ID) bool) ([]core.RetrievalHit, error)
}
func (c hostcaps) search(ctx context.Context, owner string, projectID core.ID, text string, topK int) ([]core.RetrievalHit, error) {
if c.accountGuard != nil {
if err := c.accountGuard(owner); err != nil {
return nil, err
}
}
q := core.RetrievalQuery{ProjectID: projectID, Text: text, TopK: topK}
if c.projects != nil && projectID != "" {
id, err := c.projects.resolve(ctx, c, owner, projectID)
if err != nil {
return nil, err
}
q.ProjectID = id
}
if c.authz != nil && c.authz.hasV2() {
return c.pipe.RetrieveAuthorized(ctx, core.ID(owner), q, func(d core.KnowledgeDoc) bool {
ok, err := c.authz.AuthorizeResource(extension.ResourceAuthzQuery{Actor: owner, Action: "search", ResourceType: "knowledge", ResourceID: d.ID, Owner: d.Owner, ProjectID: d.ProjectID, Source: d.Source})
return err == nil && ok
})
}
// EXT-13:进程内 Host 检索面与回合内检索同一授权口径——适配器已注册时走
// 共享通道(本人两路如常 + 放行的他人项目知识,与 HTTP 检索面一致)。
if c.authz != nil {
if rs, ok := interface{}(c.pipe).(sharedRetriever); ok {
return rs.RetrieveShared(ctx, core.ID(owner), q, func(docOwner, pid core.ID) bool {
ok, err := c.authz.Authorize(owner, pid)
return err == nil && ok
})
}
}
return c.pipe.Retrieve(ctx, core.ID(owner), q)
}
// searchDefault 与 hostapi /host/v1 检索端点同口径(不带 projectID/topK)。
func (c hostcaps) searchDefault(ctx context.Context, owner, text string) ([]core.RetrievalHit, error) {
return c.search(ctx, owner, "", text, 0)
}
// complete 经基座 LLM 路由补全(owner 的模型配置生效;未配置报错)。
func (c hostcaps) complete(ctx context.Context, owner, prompt string) (string, error) {
if c.accountGuard != nil {
if err := c.accountGuard(owner); err != nil {
return "", err
}
}
route := runtime.ResolveChatModel(core.AgentProfile{}, c.set.Effective(owner, ""))
if route.Model == "" || route.Base == "" {
return "", fmt.Errorf("llm route not configured")
}
if err := ctx.Err(); err != nil {
return "", err
}
if c.accountGuard != nil {
if err := c.accountGuard(owner); err != nil {
return "", err
}
}
return completeChat(ctx, route.Base, route.Key, route.Model, prompt)
}
// maxParseInput 与上传端点的读取上限同口径(server.upload 的 100<<20)。
const maxParseInput = 100 << 20
// parse 走基座内置解析器(EXT-5):纯计算、不落 owner 数据;不可解析由
// Warnings 显式表达,只有取消与超限是 error。
func (c hostcaps) parse(ctx context.Context, filename, mime string, content []byte) (core.ParsedDocument, error) {
if err := ctx.Err(); err != nil {
return core.ParsedDocument{}, err
}
if len(content) > maxParseInput {
return core.ParsedDocument{}, fmt.Errorf("parse input %d bytes exceeds %d limit", len(content), maxParseInput)
}
return ingest.Parse(filename, mime, content), nil
}
// inprocHost 是进程内插件命令的 owner 作用域宿主句柄(extension.Host 实现)。
// 每次分发按请求 Owner 新建;所有能力调用都以该 owner 为第一参数,插件无法越权。
// actor 是本次分发主体(通常等于 owner);AdminProvider 能力经 actor 查询
// (EXT-18)。非 dispatch 上下文(EXT-11 scoped host)actor 为空——IsAdmin 恒 false。
type inprocHost struct {
caps hostcaps
pluginID string
owner string
actor string // EXT-18:分发主体(空=非 dispatch 上下文)
conversationID core.ID // dispatch-bound, never taken from completion arguments
hostProjectID core.ID // dispatch project; rechecked against stored conversation
entrance string // dispatch entrance; model tools are "agent"
humanGrant *cmdbus.HumanGrant
}
func (h inprocHost) requireEffective() error {
if h.caps.accountGuard != nil {
return h.caps.accountGuard(h.owner)
}
return nil
}
func (h inprocHost) PluginID() string { return h.pluginID }
func (h inprocHost) Owner() string { return h.owner }
// IsAdmin 实现 EXT-18 AdminProvider:查询本次分发主体的基座平台管理角色。
// actor 为空(非 dispatch 上下文)恒 false;store 查询失败按非管理员处理
// (fail-closed——不因存储故障放行超管面)。
func (h inprocHost) IsAdmin() bool {
if h.actor == "" {
return false
}
state, err := h.caps.st.GetUserAuthState(h.actor)
if err != nil {
return false
}
return state.Enabled && state.IsAdmin
}
func (h inprocHost) legacySystemDirectoryAuthorized() bool {
if h.requireEffective() != nil {
return false
}
if h.actor == "" {
return false
}
state, err := h.caps.st.GetUserAuthState(h.actor)
return err == nil && state.Enabled && state.IsAdmin
}
// ListAccounts 保留 EXT-19 旧签名,仅供有受信分发主体的系统管理上下文;
// 项目和文档候选必须走带 locator 的 AccountDirectoryProvider。
func (h inprocHost) ListAccounts(cursor string, limit int) ([]extension.AccountEntry, string, error) {
if !h.legacySystemDirectoryAuthorized() {
return nil, "", store.ErrAccountForbidden
}
if limit <= 0 || limit > 200 {
limit = 50
}
users, err := h.caps.st.ListUsersAdmin()
if err != nil {
return nil, "", err
}
entries := make([]extension.AccountEntry, 0, len(users))
for _, u := range users {
entries = append(entries, extension.AccountEntry{Owner: u.Owner, Name: u.Name, Valid: u.Enabled})
}
// 排序+游标(owner 升序;cursor 为上一页末 owner,空=首页)。
for i := 0; i < len(entries); i++ {
for j := i + 1; j < len(entries); j++ {
if entries[j].Owner < entries[i].Owner {
entries[i], entries[j] = entries[j], entries[i]
}
}
}
start := 0
if cursor != "" {
for start < len(entries) && entries[start].Owner <= cursor {
start++
}
}
end := start + limit
if end > len(entries) {
end = len(entries)
}
next := ""
if end < len(entries) {
next = entries[end-1].Owner
}
return entries[start:end], next, nil
}
// SearchAccounts 实现 EXT-19 扩展 AccountSearcher(P3,2026-10-13):账号或
// 姓名子串检索(大小写不敏感;空 query = 首页候选)。结果按 owner 升序,
// 有效态口径与 ListAccounts 一致。
func (h inprocHost) SearchAccounts(query string, limit int) ([]extension.AccountEntry, error) {
if !h.legacySystemDirectoryAuthorized() {
return nil, store.ErrAccountForbidden
}
if limit <= 0 || limit > 200 {
limit = 50
}
users, err := h.caps.st.ListUsersAdmin()
if err != nil {
return nil, err
}
q := strings.ToLower(strings.TrimSpace(query))
entries := make([]extension.AccountEntry, 0, len(users))
for _, u := range users {
if q != "" &&
!strings.Contains(strings.ToLower(u.Owner), q) &&
!strings.Contains(strings.ToLower(u.Name), q) {
continue
}
entries = append(entries, extension.AccountEntry{Owner: u.Owner, Name: u.Name, Valid: u.Enabled})
}
sort.Slice(entries, func(i, j int) bool { return entries[i].Owner < entries[j].Owner })
if len(entries) > limit {
entries = entries[:limit]
}
return entries, nil
}
func (h inprocHost) rolePolicyActor() (store.Account, error) {
if h.actor == "" {
return store.Account{}, store.ErrAccountForbidden
}
account, err := h.caps.st.GetAccountByOwner(h.actor)
if err != nil || !account.Enabled {
return store.Account{}, store.ErrAccountForbidden
}
return account, nil
}
func (h inprocHost) CurrentRolePolicy(ctx context.Context) (extension.RolePolicySnapshot, error) {
if err := ctx.Err(); err != nil {
return extension.RolePolicySnapshot{}, err
}
actor, err := h.rolePolicyActor()
if err != nil {
return extension.RolePolicySnapshot{}, err
}
return h.RolePolicyForUser(ctx, actor.ID)
}
func (h inprocHost) RolePolicyForUser(ctx context.Context, userID string) (extension.RolePolicySnapshot, error) {
if err := ctx.Err(); err != nil {
return extension.RolePolicySnapshot{}, err
}
if _, err := h.rolePolicyActor(); err != nil {
return extension.RolePolicySnapshot{}, err
}
return rolePolicySnapshotForUser(h.caps.st, userID)
}
func rolePolicySnapshotForUser(st *store.Store, userID string) (extension.RolePolicySnapshot, error) {
account, err := st.GetAccount(userID)
if err != nil {
return extension.RolePolicySnapshot{}, err
}
epoch, err := st.AuthorityEpoch()
if err != nil {
return extension.RolePolicySnapshot{}, err
}
snapshot := extension.RolePolicySnapshot{SchemaVersion: extension.RolePolicySchemaVersion,
UserID: account.ID, Owner: account.Owner, AccountEnabled: account.Enabled,
AuthorityEpoch: epoch, Permissions: map[string]extension.RolePermission{}}
linked, err := authzsvc.NewService(st).EffectiveLinkedCapabilities(account.Owner)
if err != nil {
return extension.RolePolicySnapshot{}, err
}
snapshot.Linked = extension.LinkedCapabilities{ProjectCreate: linked.ProjectCreate, Source: linked.Source,
Actions: linked.LinkedActions, Conflicts: linked.Conflicts}
view, err := st.RoleForUser(userID)
if err != nil {
return extension.RolePolicySnapshot{}, err
}
if view.Binding == nil || view.Role == nil {
return snapshot, nil
}
snapshot.BindingID = view.Binding.BindingID
snapshot.RoleID = view.Role.RoleID
snapshot.RoleName = view.Role.Name
snapshot.RoleDescription = view.Role.Description
snapshot.RoleEnabled = view.Role.Enabled
snapshot.RoleVersion = view.Role.RoleVersion
permissions, err := st.RolePermissions(view.Role.RoleID)
if err != nil {
return extension.RolePolicySnapshot{}, err
}
for _, permission := range permissions {
snapshot.Permissions[permission.ActionID] = extension.RolePermission{Allowed: permission.Allowed, CapAllowed: permission.CapAllowed}
}
return snapshot, nil
}
func (h inprocHost) RolePolicyForOwner(ctx context.Context, owner string) (extension.RolePolicySnapshot, error) {
if _, err := h.rolePolicyActor(); err != nil {
return extension.RolePolicySnapshot{}, err
}
account, err := h.caps.st.GetAccountByOwner(owner)
if err != nil {
return extension.RolePolicySnapshot{}, err
}
return h.RolePolicyForUser(ctx, account.ID)
}
func (h inprocHost) CheckRoleAction(ctx context.Context, userID, actionID string) (extension.RoleActionDecision, error) {
decision := extension.RoleActionDecision{ActionID: actionID}
if _, ok := catalog.DescriptorByID(actionID); !ok {
decision.Reason = "unknown-action"
return decision, nil
}
snapshot, err := h.RolePolicyForUser(ctx, userID)
if err != nil {
return decision, err
}
decision.RoleID = snapshot.RoleID
decision.AuthorityEpoch = snapshot.AuthorityEpoch
if !snapshot.AccountEnabled {
decision.Reason = "account-disabled"
return decision, nil
}
if snapshot.RoleID == "" {
decision.Reason = "role-unassigned"
return decision, nil
}
if !snapshot.RoleEnabled {
decision.Reason = "role-disabled"
return decision, nil
}
permission, ok := snapshot.Permissions[actionID]
if !ok {
decision.Reason = "action-unconfigured"
return decision, nil
}
decision.Allowed = permission.Allowed
decision.CapAllowed = permission.CapAllowed
if !permission.CapAllowed {
decision.Reason = "role-cap"
} else if !permission.Allowed {
decision.Reason = "role-default-deny"
}
return decision, nil
}
func (h inprocHost) CheckCurrentSystemAction(ctx context.Context, actionID string) (extension.SystemActionDecision, error) {
if err := ctx.Err(); err != nil {
return extension.SystemActionDecision{}, err
}
if _, err := h.rolePolicyActor(); err != nil {
return extension.SystemActionDecision{}, err
}
decision := authzsvc.NewService(h.caps.st).AuthorizeSystem(h.actor, actionID)
return extension.SystemActionDecision{ActionID: actionID, Allowed: decision.Allowed,
AuthorityEpoch: decision.AuthorityEpoch, Reason: decision.Reason}, nil
}
func (h inprocHost) currentCapabilityExplanation(ctx context.Context) (store.CapabilityExplanation, extension.CapabilitySubject, error) {
if err := ctx.Err(); err != nil {
return store.CapabilityExplanation{}, extension.CapabilitySubject{}, err
}
if _, err := h.rolePolicyActor(); err != nil {
return store.CapabilityExplanation{}, extension.CapabilitySubject{}, err
}
explanation, err := authzsvc.NewService(h.caps.st).ExplainCapabilities(h.actor)
if err != nil {
return store.CapabilityExplanation{}, extension.CapabilitySubject{}, err
}
subject := extension.CapabilitySubject{SchemaVersion: extension.CapabilityExplainSchemaVersion,
UserID: explanation.UserID, Owner: explanation.Owner, AuthorityEpoch: explanation.AuthorityEpoch,
RoleID: explanation.RoleID, RoleName: explanation.RoleName, RoleEnabled: explanation.RoleEnabled}
return explanation, subject, nil
}
func capabilityActionExplains(in []store.CapabilityActionExplain) []extension.CapabilityActionExplain {
out := make([]extension.CapabilityActionExplain, 0, len(in))
for _, action := range in {
out = append(out, extension.CapabilityActionExplain(action))
}
return out
}
func (h inprocHost) CurrentCapabilityExplain(ctx context.Context) (extension.CapabilityExplain, error) {
explanation, subject, err := h.currentCapabilityExplanation(ctx)
if err != nil {
return extension.CapabilityExplain{}, err
}
capability := explanation.Capability
return extension.CapabilityExplain{CapabilitySubject: subject,
ProjectCreate: extension.CapabilityActionExplain(capability.ProjectCreate),
Linked: capabilityActionExplains(capability.Linked),
Conflicts: append([]string{}, capability.Conflicts...), MigrationNote: capability.MigrationNote}, nil
}
func (h inprocHost) CurrentMemberBasicCollaborate(ctx context.Context) (extension.MemberBasicCollaborate, error) {
explanation, subject, err := h.currentCapabilityExplanation(ctx)
if err != nil {
return extension.MemberBasicCollaborate{}, err
}
return extension.MemberBasicCollaborate{CapabilitySubject: subject,
Actions: capabilityActionExplains(explanation.MemberBasicCollaborate.Actions)}, nil
}
func (h inprocHost) CurrentDocumentQualify(ctx context.Context) (extension.DocumentQualify, error) {
explanation, subject, err := h.currentCapabilityExplanation(ctx)
if err != nil {
return extension.DocumentQualify{}, err
}
duties := make([]extension.DocumentDutyQualify, 0, len(explanation.DocumentQualify.Duties))
for _, duty := range explanation.DocumentQualify.Duties {
duties = append(duties, extension.DocumentDutyQualify(duty))
}
return extension.DocumentQualify{CapabilitySubject: subject, Duties: duties}, nil
}
func (h inprocHost) requireRoleAdminAction(actionID string) error {
if h.actor == "" || h.owner != h.actor {
return store.ErrAccountForbidden
}
decision := authzsvc.NewService(h.caps.st).AuthorizeSystem(h.actor, actionID)
if !decision.Allowed {
return store.ErrAccountForbidden
}
return nil
}
func roleAdminRecord(view store.RoleAdminView) extension.RoleAdminRecord {
out := extension.RoleAdminRecord{RoleID: view.RoleID, Name: view.Name, Description: view.Description,
Enabled: view.Enabled, RoleVersion: view.RoleVersion, BindingCount: view.BindingCount,
Permissions: map[string]extension.RolePermission{}}
for _, permission := range view.Permissions {
out.Permissions[permission.ActionID] = extension.RolePermission{Allowed: permission.Allowed, CapAllowed: permission.CapAllowed}
}
return out
}
func (h inprocHost) ListRoles(ctx context.Context) ([]extension.RoleAdminRecord, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if err := h.requireRoleAdminAction("system.roles.view"); err != nil {
return nil, err
}
result := []extension.RoleAdminRecord{}
cursor := ""
for {
roles, next, err := h.caps.st.ListRoles("", "fuzzy", "all", "", cursor, 100)
if err != nil {
return nil, err
}
for _, role := range roles {
result = append(result, roleAdminRecord(role))
}
if next == "" {
return result, nil
}
cursor = next
}
}
func (h inprocHost) CreateRole(ctx context.Context, name, description string) (extension.RoleAdminRecord, error) {
if err := ctx.Err(); err != nil {
return extension.RoleAdminRecord{}, err
}
if err := h.requireRoleAdminAction("system.roles.manage"); err != nil {
return extension.RoleAdminRecord{}, err
}
role, err := h.caps.st.CreateRole(h.actor, store.RoleCreate{Name: name, Description: description})
if err != nil {
return extension.RoleAdminRecord{}, err
}
view, err := h.caps.st.RoleAdmin(role.RoleID)
return roleAdminRecord(view), err
}
func (h inprocHost) SetRolePermission(ctx context.Context, roleID, actionID string, allowed, capAllowed bool, key string) (extension.RoleAdminRecord, error) {
if err := ctx.Err(); err != nil {
return extension.RoleAdminRecord{}, err
}
if err := h.requireRoleAdminAction("system.permissions.manage"); err != nil {
return extension.RoleAdminRecord{}, err
}
view, err := h.caps.st.RoleAdmin(roleID)
if err != nil {
return extension.RoleAdminRecord{}, err
}
changes := make([]store.RolePermissionChange, 0, len(view.Permissions)+1)
found := false
for _, permission := range view.Permissions {
if permission.ActionID == actionID {
found = true
if permission.Allowed == allowed && permission.CapAllowed == capAllowed {
return roleAdminRecord(view), nil
}
permission.Allowed, permission.CapAllowed = allowed, capAllowed
}
changes = append(changes, store.RolePermissionChange{ActionID: permission.ActionID, Allowed: permission.Allowed, CapAllowed: permission.CapAllowed})
}
if !found {
changes = append(changes, store.RolePermissionChange{ActionID: actionID, Allowed: allowed, CapAllowed: capAllowed})
}
if key == "" {
return extension.RoleAdminRecord{}, store.ErrInvalidRolePolicy
}
if _, err := h.caps.st.ReplaceRolePermissions(h.actor, roleID, view.RoleVersion, changes, key+":"+strconv.FormatInt(view.RoleVersion, 10)); err != nil {
return extension.RoleAdminRecord{}, err
}
view, err = h.caps.st.RoleAdmin(roleID)
return roleAdminRecord(view), err
}
func (h inprocHost) AssignRoleByOwner(ctx context.Context, owner, roleID, key string) (extension.RoleBindingResult, error) {
if err := ctx.Err(); err != nil {
return extension.RoleBindingResult{}, err
}
if err := h.requireRoleAdminAction("system.roles.assign"); err != nil {
return extension.RoleBindingResult{}, err
}
target, err := h.caps.st.GetAccountByOwner(owner)
if err != nil {
return extension.RoleBindingResult{}, err
}
current, err := h.caps.st.RoleForUser(target.ID)
if err != nil {
return extension.RoleBindingResult{}, err
}
if current.Binding != nil && current.Binding.RoleID == roleID {
return extension.RoleBindingResult{BindingID: current.Binding.BindingID, UserID: target.ID, RoleID: roleID,
UserStateVersion: current.Binding.UserStateVersion, AuthorityEpoch: current.Binding.AuthorityEpoch}, nil
}
if key == "" {
return extension.RoleBindingResult{}, store.ErrInvalidRolePolicy
}
binding, err := h.caps.st.AssignUserRole(h.actor, target.ID, roleID, target.StateVersion, key+":"+strconv.FormatInt(target.StateVersion, 10))
if err != nil {
return extension.RoleBindingResult{}, err
}
return extension.RoleBindingResult{BindingID: binding.BindingID, UserID: binding.UserID, RoleID: binding.RoleID,
UserStateVersion: binding.UserStateVersion, AuthorityEpoch: binding.AuthorityEpoch}, nil
}
func (h inprocHost) SetRoleEnabled(ctx context.Context, roleID string, enabled bool) (extension.RoleAdminRecord, error) {
if err := ctx.Err(); err != nil {
return extension.RoleAdminRecord{}, err
}
if err := h.requireRoleAdminAction("system.roles.manage"); err != nil {
return extension.RoleAdminRecord{}, err
}
view, err := h.caps.st.RoleAdmin(roleID)
if err != nil {
return extension.RoleAdminRecord{}, err
}
if view.Enabled == enabled {
return roleAdminRecord(view), nil
}
role, err := h.caps.st.PatchRole(h.actor, roleID, view.RoleVersion, store.RolePatch{Enabled: &enabled})
if err != nil {
return extension.RoleAdminRecord{}, err
}
view, err = h.caps.st.RoleAdmin(role.RoleID)
return roleAdminRecord(view), err
}
func (h inprocHost) ImportRoleAuthority(ctx context.Context, in extension.RoleAuthorityImport, key string) (extension.RoleAuthorityImportResult, error) {
if err := ctx.Err(); err != nil {
return extension.RoleAuthorityImportResult{}, err
}
if err := h.requireRoleAdminAction("system.permissions.manage"); err != nil {
return extension.RoleAuthorityImportResult{}, err
}
roles := make([]store.RoleAuthorityImportRole, 0, len(in.Roles))
for _, role := range in.Roles {
permissions := make([]store.RolePermissionChange, 0, len(role.Permissions))
for actionID, permission := range role.Permissions {
permissions = append(permissions, store.RolePermissionChange{ActionID: actionID,
Allowed: permission.Allowed, CapAllowed: permission.CapAllowed})
}
sort.Slice(permissions, func(i, j int) bool { return permissions[i].ActionID < permissions[j].ActionID })
roles = append(roles, store.RoleAuthorityImportRole{RoleID: role.RoleID, Name: role.Name,
Description: role.Description, Enabled: role.Enabled, RoleVersion: role.RoleVersion,
Permissions: permissions, Owners: append([]string(nil), role.Owners...)})
}
result, err := h.caps.st.ImportRoleAuthority(h.actor, store.RoleAuthorityImport{
SourceSchemaVersion: in.SourceSchemaVersion, SourceSHA256: in.SourceSHA256, Roles: roles}, key)
if err != nil {
return extension.RoleAuthorityImportResult{}, err
}
return extension.RoleAuthorityImportResult{AuthorityEpoch: result.AuthorityEpoch,
ImportedRoles: result.ImportedRoles, ImportedUsers: result.ImportedUsers}, nil
}
func (h inprocHost) ReadRoleAuthorityImport(ctx context.Context, sourceSHA string) (extension.RoleAuthorityImportProjection, error) {
if err := ctx.Err(); err != nil {
return extension.RoleAuthorityImportProjection{}, err
}
if err := h.requireRoleAdminAction("system.permissions.manage"); err != nil {
return extension.RoleAuthorityImportProjection{}, err
}
staged, err := h.caps.st.ReadRoleAuthorityImport(h.actor, sourceSHA)
if err != nil {
return extension.RoleAuthorityImportProjection{}, err
}
roles := make([]extension.RoleAdminRecord, 0, len(staged.Roles))
bindings := map[string]string{}
for _, role := range staged.Roles {
permissions := map[string]extension.RolePermission{}
for _, permission := range role.Permissions {
permissions[permission.ActionID] = extension.RolePermission{Allowed: permission.Allowed, CapAllowed: permission.CapAllowed}
}
roles = append(roles, extension.RoleAdminRecord{RoleID: role.RoleID, Name: role.Name,
Description: role.Description, Enabled: role.Enabled, RoleVersion: role.RoleVersion,
BindingCount: len(role.Owners), Permissions: permissions})
for _, owner := range role.Owners {
bindings[owner] = role.RoleID
}
}
epoch, err := h.caps.st.AuthorityEpoch()
if err != nil {
return extension.RoleAuthorityImportProjection{}, err
}
return extension.RoleAuthorityImportProjection{SourceSHA256: staged.SourceSHA256,
AuthorityEpoch: epoch, Roles: roles, Bindings: bindings}, nil
}
func (h inprocHost) ActivateRoleAuthorityImport(ctx context.Context, sourceSHA string, expectedEpoch int64) (extension.RoleAuthorityImportResult, error) {
if err := ctx.Err(); err != nil {
return extension.RoleAuthorityImportResult{}, err
}
if err := h.requireRoleAdminAction("system.permissions.manage"); err != nil {
return extension.RoleAuthorityImportResult{}, err
}
result, err := h.caps.st.ActivateRoleAuthorityImport(h.actor, sourceSHA, expectedEpoch)
if err != nil {
return extension.RoleAuthorityImportResult{}, err
}
return extension.RoleAuthorityImportResult{AuthorityEpoch: result.AuthorityEpoch,
ImportedRoles: result.ImportedRoles, ImportedUsers: result.ImportedUsers}, nil
}
func (h inprocHost) KVPut(key, value string) error {
if err := h.requireEffective(); err != nil {
return err
}
return h.caps.st.PutPluginKVAccountChecked(h.owner, h.pluginID, key, value, h.caps.allowImplicitLocal)
}
func (h inprocHost) KVGet(key string) (string, bool, error) {
if err := h.requireEffective(); err != nil {
return "", false, err
}
return h.caps.st.GetPluginKV(h.pluginID, h.owner, key)
}
func (h inprocHost) Audit(action, target, evidence string) error {
if err := h.requireEffective(); err != nil {
return err
}
if strings.HasPrefix(action, "model.context_manifest") {
return fmt.Errorf("extension: model context manifest is a reserved host receipt")
}
return h.caps.st.AppendAuditEventAccountChecked(h.owner, store.AuditEvent{
Owner: h.owner, Actor: "plugin:" + h.pluginID,
Action: action, Target: target, Evidence: evidence,
}, h.caps.allowImplicitLocal)
}
func (h inprocHost) Complete(ctx context.Context, prompt string) (string, error) {
if err := h.requireEffective(); err != nil {
return "", err
}
return h.caps.complete(ctx, h.owner, prompt)
}
func (h inprocHost) KnowledgeSearch(ctx context.Context, projectID core.ID, text string, topK int) ([]core.RetrievalHit, error) {
if err := h.requireEffective(); err != nil {
return nil, err
}
return h.caps.search(ctx, h.owner, projectID, text, topK)
}
// knowledgeSource preserves ordinary host Source semantics. A lifecycle provider
// introduces shared publication provenance, so plugins may only assert their own
// namespace; another plugin cannot impersonate an RDM published row.
func (h inprocHost) knowledgeSource(source string) error {
if h.caps.projects == nil || !h.caps.projects.active() {
return nil
}
prefix := "plugin:" + h.pluginID
if source == prefix || strings.HasPrefix(source, prefix+":") {
return nil
}
return fmt.Errorf("extension: knowledge source must belong to plugin %q", h.pluginID)
}
// KnowledgeWrite 强制 owner 归属:Owner 改写为本次分发 owner,Source 为空补
// "plugin:<id>";embed=false 只写全文行,embed=true 走 IndexChunks 建向量
// (embedding 失败降级纯全文,不阻断写入)。
func (h inprocHost) KnowledgeWrite(ctx context.Context, docs []core.KnowledgeDoc, embed bool) error {
if err := h.requireEffective(); err != nil {
return err
}
if len(docs) == 0 {
return nil
}
src := "plugin:" + h.pluginID
for i := range docs {
docs[i].Owner = core.ID(h.owner)
if docs[i].Source == "" {
docs[i].Source = src
}
if err := h.knowledgeSource(docs[i].Source); err != nil {
return err
}
}
if !embed {
return h.caps.st.AddKnowledgeAccountChecked(h.owner, h.caps.allowImplicitLocal, docs...)
}
return h.caps.pipe.IndexChunksAccountChecked(ctx, core.ID(h.owner), docs, h.caps.allowImplicitLocal)
}
// KnowledgePublish replaces refs and their vectors within the publication scope.
// A registered lifecycle scopes replacement to the exact owner/project/plugin;
// ordinary unregistered Hosts retain the legacy cross-source ref semantics.
func (h inprocHost) KnowledgePublish(ctx context.Context, docs []core.KnowledgeDoc, embed bool) error {
if err := h.requireEffective(); err != nil {
return err
}
if len(docs) == 0 {
return nil
}
for i := range docs {
docs[i].Owner = core.ID(h.owner)
if docs[i].Source == "" {
docs[i].Source = "plugin:" + h.pluginID
}
if err := h.knowledgeSource(docs[i].Source); err != nil {
return err
}
}
scoped := h.caps.projects != nil && h.caps.projects.active()
if embed {
return h.caps.pipe.PublishChunksAccountChecked(ctx, core.ID(h.owner), h.pluginID, scoped, docs, h.caps.allowImplicitLocal)
}
return h.caps.st.ReplaceKnowledgeAccountChecked(ctx, h.owner, h.pluginID, scoped, h.caps.allowImplicitLocal, docs...)
}
// KnowledgeDelete 撤销本插件来源(EXT-9):前缀强制 plugin:<自身ID>——manual
// 或他人来源拒绝(fail-closed);删行+向量,幂等。
func (h inprocHost) KnowledgeDelete(ctx context.Context, sourcePrefix string) error {
if err := h.requireEffective(); err != nil {
return err
}
if sourcePrefix != "plugin:"+h.pluginID {
return fmt.Errorf("ext-9: plugins may only delete their own source prefix, want %q", "plugin:"+h.pluginID)
}
_, err := h.caps.st.DeleteKnowledgeBySourceAccountChecked(ctx, h.owner, sourcePrefix, h.caps.allowImplicitLocal)
return err
}
// TaskCreate/TaskUpdate 后台任务台账:基座只记进度/状态,执行体由下游自跑(H5)。
func (h inprocHost) TaskCreate(kind, title, conversationID string) (string, error) {
if err := h.requireEffective(); err != nil {
return "", err
}
t, err := h.caps.st.CreateBackgroundTaskAccountChecked(h.owner, kind, title, conversationID, h.caps.allowImplicitLocal)
if err != nil {
return "", err
}
return t.ID, nil
}
func (h inprocHost) TaskUpdate(taskID, status string, progress int, errText string) error {
if err := h.requireEffective(); err != nil {
return err
}
return h.caps.st.UpdateBackgroundTaskAccountChecked(h.owner, taskID, status, progress, errText, h.caps.allowImplicitLocal)
}
// ArtifactGet/ArtifactContent 工件只读(R1-3 ingest 读原件;写入属完整集)。
func (h inprocHost) ArtifactGet(artifactID string) (core.Artifact, error) {
if err := h.requireEffective(); err != nil {
return core.Artifact{}, err
}
return h.caps.st.GetArtifact(h.owner, artifactID)
}
func (h inprocHost) ArtifactContent(artifactID string) ([]byte, error) {
if err := h.requireEffective(); err != nil {
return nil, err
}
return h.caps.st.ArtifactContent(h.owner, artifactID)
}
// ParseDocument 内置文档解析(EXT-5):委托 hostcaps.parse;纯计算不经存储,
// owner 作用域由调用方经 ArtifactContent 取字节时保证。
func (h inprocHost) ParseDocument(ctx context.Context, filename, mime string, content []byte) (core.ParsedDocument, error) {
if err := h.requireEffective(); err != nil {
return core.ParsedDocument{}, err
}
return h.caps.parse(ctx, filename, mime, content)
}
// ---- EXT-17 Surface 能力(可选):nil 槽=能力缺失,明确拒绝不降级 ----
func (h inprocHost) surface() (*surfacesvc.Service, error) {
if err := h.requireEffective(); err != nil {
return nil, err
}
if h.caps.surfaces == nil {
return nil, surfacesvc.ErrUnavailable
}
return h.caps.surfaces, nil
}
func (h inprocHost) SurfaceSave(ctx context.Context, req extension.SurfaceSaveRequest) (core.SurfaceSnapshot, error) {
svc, err := h.surface()
if err != nil {
return core.SurfaceSnapshot{}, err
}
return svc.SubmitAccountChecked(ctx, h.owner, h.pluginID, surfacesvc.SubmitRequest{
SurfaceID: req.SurfaceID, ConversationID: req.ConversationID, SourceRunID: req.SourceRunID,
BaseRevision: req.BaseRevision, ObservedEpoch: req.ObservedEpoch,
ContextVersion: req.ContextVersion, SemanticText: req.SemanticText, Spec: req.Spec,
}, h.caps.allowImplicitLocal)
}
func (h inprocHost) SurfaceGet(ctx context.Context, surfaceID core.ID) (core.SurfaceSnapshot, error) {
svc, err := h.surface()
if err != nil {
return core.SurfaceSnapshot{}, err
}
return svc.Get(ctx, h.owner, surfaceID)
}
func (h inprocHost) SurfaceList(ctx context.Context, conversationID core.ID) ([]core.SurfaceMeta, error) {
svc, err := h.surface()
if err != nil {
return nil, err
}
return svc.List(ctx, h.owner, conversationID)
}
func (h inprocHost) SurfaceActivation(ctx context.Context) (bool, int64, error) {
svc, err := h.surface()
if err != nil {
return false, 0, err
}
row, err := svc.Activation(ctx, h.owner, h.pluginID)
return row.Enabled, row.Epoch, err
}