Files
HomeAgent/internal/plugin/proc/capability.go
JianFeeeee 144564f5c2 feat(knowledge): 分类过滤与多模态打通到工具/插件边界
- internal/sdk:KnowledgeAPI 增加 SearchIn/AddWithMedia/AttachMedia/
  ReindexDense/DenseStats;新增 MediaAPI(Put/Stat/Get)与 PluginSDK.Media(),
  registry 装配。**公开 SDK 契约一字未动** —— third_party/homeagent-sdk/sdk/
  的 diff 恒为 0(发布纪律的硬约束),走 internal/sdk 这条明确不受冻结约束
  的内部扩展路径。
- proc:新增 knowledge.addWithMedia(正文与媒体清单都走共享内存,与
  doc.insertWithMedia 同形);knowledge.search 支持 category。
- corehandler 用**局部接口 + 类型断言**取扩展能力,而非直接引 internal/sdk:
  后者已依赖 internal/plugin 的类型,直接引会成环(CoreSDK 注释已警示)。
  断言失败明确报"能力不可用",不静默退化成"媒体已写入"。
- agent 工具面:knowledge_search 增加 category;knowledge_create 增加
  media_digests(复用 doc_commit 的 digest 前缀解析 + Stat 回读 MIME)。
  顺带修 knowledge_search 的分类前缀重复(Name 已含分类,旧代码又拼一次,
  实测输出 tech/go/tech/go/并发)。
- agent 启动接线多模态空间,顺序为先接线再 ReindexDense(否则首次启动
  算出的向量因 MediaStore 未就绪而不落盘)。
2026-09-26 14:20:19 +08:00

279 lines
10 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 proc
import (
"fmt"
"sort"
"strings"
)
// 权限梯度:外部插件可调用哪些内核 method(§3.8)。
//
// 迁移前,「外部插件拿不到 Selftest/Supervisor/Tracker」是 C ABI 表达能力的
// **意外产物**——C 结构体不好传函数指针,于是这些能力自然到不了插件侧。
// 那是运气,不是策略:任何人给 dispatch 加个 case 就能捅穿。
//
// 迁移后要变成**显式声明并强制的策略**,分三道闸:
//
// 1. 类型层(internal/plugin/proc_core.go):procCore 用命名字段持有内核 SDK,
// 不嵌入 —— 未在收窄面显式写出的方法根本不存在,编译期就拿不到。
// 2. 能力集(本文件):method 划入 capability 组,manifest 未声明的组被拒。
// 3. RPC 边界:被拒时返回**明确错误**而非静默忽略——插件作者能立刻知道
// 「这个能力没给我」,而不是调用成功但什么也没发生。
//
// 第 3 条针对的是一类真实故障:C ABI 时代 case 23/24(事件订阅)是空实现,
// 返回成功但永远收不到事件(§1.3 的「给不了」而非「不给」)。
// Capability 是一组相关 method 的权限单元。
//
// 粒度选择:按**能力域**而非单个 method 划分。逐 method 授权看似更精细,
// 但插件作者要在 manifest 里列 60 个名字,且内核加 method 时所有 manifest 都得改。
type Capability string
const (
// CapCore 是无需声明即可用的基础能力:注册自身工具/阶段/通道、
// 读写自己的配置、共享段锁仲裁。没有这些插件无法工作。
CapCore Capability = "core"
// CapIO 注入输入到 agent 主循环(可影响对话流)。
CapIO Capability = "io"
// CapMemory 图记忆读写。
CapMemory Capability = "memory"
// CapDocMemory 文档记忆读写。
CapDocMemory Capability = "doc_memory"
// CapKnowledge 知识库读写。
CapKnowledge Capability = "knowledge"
// CapTextMemory 文本记忆追加。
CapTextMemory Capability = "text_memory"
// CapLLM 切换 LLM 源(影响全局行为)。
CapLLM Capability = "llm"
// CapSocial 社交图读取。
CapSocial Capability = "social"
// CapEvents 订阅内核事件。
CapEvents Capability = "events"
// CapPluginMgr 管理其他插件(重载/查询禁用状态)。
//
// 这是**最敏感**的一组:能重载其他插件意味着能间接影响它们的状态。
CapPluginMgr Capability = "plugin_mgr"
// CapCrossPluginSettings 读写**其他插件**的配置与内核核心配置。
//
// 与 CapCore 里的「读写自己的配置」区分开:跨插件配置读写能改别人的行为,
// 核心配置读写能改内核行为。
CapCrossPluginSettings Capability = "settings_cross"
)
// methodCapability 把每个 method 映射到所需能力。
//
// ❗ 新增 method 时必须在此登记,否则 capabilityOf 返回 CapCore
// (最宽松),等于绕过权限检查。checkAllMethodsClassified 测试守着这一点。
var methodCapability = map[string]Capability{
// ---- 基础能力(无需声明)----
MethodHandshake: CapCore,
MethodToolRegister: CapCore,
MethodStageRegister: CapCore,
MethodOutputRegister: CapCore,
MethodAPIRegister: CapCore,
MethodInputRegister: CapCore,
MethodStageLock: CapCore,
MethodStageUnlock: CapCore,
// 共享槽池申请/归还:内部传输层能力,等同于核心基础能力。
MethodArenaAlloc: CapCore,
MethodArenaFree: CapCore,
// 自身配置读写与元信息属基础能力
MethodSettingsGet: CapCore,
MethodSettingsSet: CapCore,
MethodSettingsList: CapCore,
MethodSettingsDefs: CapCore,
MethodSettingsRegisterDef: CapCore,
MethodSettingsDataDir: CapCore,
// 生命周期自述(插件声明自己是否可自动重启)
MethodLifecycleAutoRestart: CapCore,
// 多模态内容块注入是工具返回值的一部分,不越权
MethodIOSetToolBlocks: CapCore,
// ---- IO 注入 ----
MethodIOInjectText: CapIO,
MethodIOInjectInterrupt: CapIO,
MethodIOInjectTextNoMem: CapIO,
MethodIOInjectSync: CapIO,
// 带媒体的注入与纯文本注入同一权限组:能不能发起一轮对话是 IO 能力,
// 带不带图不改变这个判断。
MethodIOInjectMedia: CapIO,
MethodIOInjectMediaSync: CapIO,
MethodIOInjectInterruptMedia: CapIO,
// ---- 图记忆 ----
MethodMemoryRecall: CapMemory,
MethodMemoryCommit: CapMemory,
MethodMemoryIntrospect: CapMemory,
MethodMemoryMerge: CapMemory,
MethodMemoryPurge: CapMemory,
// ---- 文档记忆 ----
MethodDocQuery: CapDocMemory,
MethodDocInsert: CapDocMemory,
MethodDocRemove: CapDocMemory,
MethodDocStats: CapDocMemory,
// 带媒体写入与普通写入同权限:都是往文档记忆里写东西。
MethodDocInsertMedia: CapDocMemory,
// ---- 知识库 ----
MethodKnowledgeSearch: CapKnowledge,
MethodKnowledgeAdd: CapKnowledge,
MethodKnowledgeAddMedia: CapKnowledge,
MethodKnowledgeList: CapKnowledge,
// ---- 文本记忆 ----
MethodTextMemoryAppend: CapTextMemory,
// ---- LLM ----
MethodLLMListSources: CapLLM,
MethodLLMSetSource: CapLLM,
MethodLLMCurrentSource: CapLLM,
// ---- 社交图 ----
MethodSocialGetPerson: CapSocial,
MethodSocialGetNetwork: CapSocial,
MethodSocialGetTrait: CapSocial,
MethodSocialGetRelation: CapSocial,
MethodSocialListPersons: CapSocial,
// ---- 事件 ----
MethodEventsSubscribe: CapEvents,
MethodEventsUnsubscribe: CapEvents,
// ---- 插件管理 ----
MethodPluginReloadOne: CapPluginMgr,
MethodPluginListLoaded: CapPluginMgr,
MethodPluginIsDisabled: CapPluginMgr,
// ---- 跨插件 / 核心配置 ----
MethodSettingsGetCore: CapCrossPluginSettings,
MethodSettingsSetCore: CapCrossPluginSettings,
MethodSettingsListCore: CapCrossPluginSettings,
MethodSettingsGetPlugin: CapCrossPluginSettings,
MethodSettingsSetPlugin: CapCrossPluginSettings,
MethodSettingsListPlugin: CapCrossPluginSettings,
MethodSettingsDump: CapCrossPluginSettings,
MethodSettingsPlugins: CapCrossPluginSettings,
}
// withheldCapabilities 是**刻意不提供给外部插件**的内核内部机制(§3.8 最后一行)。
//
// 这些没有对应的 method 常量——不是"忘了加",是决定不加。
// 列在这里是为了让决策可见:读代码的人能看到边界在哪,而不是从
// 「protocol.go 里没有」这个负面事实去推断。
//
// 类型层已经挡住了(procCore 不暴露这些访问器),本表是文档 + 测试锚点。
var withheldCapabilities = map[string]string{
"SelftestAPI": "虚拟实例自检 —— 能构造内核实例,等于绕过全部权限边界",
"SupervisorAPI": "进程监管 —— 能启停 worker,等于控制内核生命周期",
"TrackerAPI": "变更追踪 —— 内核 overlay 文件系统的内部机制",
"StatusAPI": "内核状态面 —— 暴露内部运行时细节",
"AdapterAPI": "LLM 适配器管理 —— 能改写请求/响应链路",
"ConfigAPI": "内核配置对象 —— 与 settings 的受控读写不同,这是直接持有",
"ToolAPI": "工具表直接操作 —— 能注销其他插件的工具(注册自己的工具走 tool.register,那是 core)",
"IndexerAPI": "记忆索引器 —— 内核记忆管线的内部组件",
"OutputChanRaw": "输出通道原始消费 —— 已由 output.invoke 的声明式注册替代",
"EventPublish": "事件发布 —— 只给订阅(events.subscribe),不给伪造内核事件",
}
// capabilityOf 返回 method 所需能力。
//
// 未登记的 method 返回 (CapCore, false):ok=false 让调用方能区分
// 「明确划为基础能力」与「漏登记」,测试据此拦住漏登记。
func capabilityOf(method string) (Capability, bool) {
cap, ok := methodCapability[method]
if !ok {
return CapCore, false
}
return cap, true
}
// capabilitySet 是某个插件被授予的能力集合。
type capabilitySet struct {
granted map[Capability]bool
// unrestricted 为真时跳过检查(未声明 capabilities 的插件,向后兼容)。
unrestricted bool
}
// newCapabilitySet 从 manifest 声明构造能力集。
//
// **空声明 = 不受限**,而不是「只有 core」。理由:17 个存量插件的 plugin.json
// 都没有 capabilities 字段,若空声明当作最小权限,它们会全部失去 IO 注入、
// 记忆读写等能力而**静默降级**——这违反「外部插件零改动」的硬约束。
//
// 收紧的路径是让插件显式声明,而非默默拒绝老插件。
func newCapabilitySet(declared []string) *capabilitySet {
if len(declared) == 0 {
return &capabilitySet{unrestricted: true}
}
s := &capabilitySet{granted: map[Capability]bool{CapCore: true}}
for _, d := range declared {
s.granted[Capability(strings.TrimSpace(d))] = true
}
return s
}
// allows 判断是否允许调用某 method。
func (s *capabilitySet) allows(method string) (bool, Capability) {
cap, registered := capabilityOf(method)
if !registered {
// 漏登记的 method 按基础能力放行(保守:不因内核疏漏拦住插件),
// 但由测试保证这种情况不存在。
return true, CapCore
}
if s == nil || s.unrestricted {
return true, cap
}
if cap == CapCore {
return true, cap
}
return s.granted[cap], cap
}
// KnownCapabilities 返回全部可声明的能力名(供 manifest 校验与文档生成)。
func KnownCapabilities() []string {
seen := map[Capability]bool{}
for _, c := range methodCapability {
seen[c] = true
}
out := make([]string, 0, len(seen))
for c := range seen {
if c == CapCore {
continue // core 无需声明
}
out = append(out, string(c))
}
sort.Strings(out)
return out
}
// WithheldCapabilities 返回刻意不提供的能力清单(供文档与诊断)。
func WithheldCapabilities() map[string]string {
out := make(map[string]string, len(withheldCapabilities))
for k, v := range withheldCapabilities {
out[k] = v
}
return out
}
// errCapabilityDenied 构造被拒错误。
//
// 消息包含三要素:被拒的 method、缺的能力名、如何补救。
// 静默忽略或含糊的「失败」会让插件作者以为是自己参数错了。
func errCapabilityDenied(plugin, method string, cap Capability) error {
return fmt.Errorf(
"插件 %s 调用 %s 被拒:缺少 %q 能力。"+
"请在 plugin.json 的 capabilities 数组中声明它(可用能力:%s)",
plugin, method, cap, strings.Join(KnownCapabilities(), ", "))
}