Files
HomeAgent/internal/plugin/proc/capability.go
JianFeeeee ca584bda3f refactor(shm): 内核独占共享槽池,插件经 RPC 申请/归还(§13.2/§13.3)
推翻前两版跨进程分配器设计,根因是"共享内存里放了只被单一进程
更新的可变游标":

- v1 在 SuperBlock 放 arenaUsed 游标,内核 CAS bump。但插件模板里
  arenaUsed 是**进程本地变量**,两进程各自 bump 必写同一段内存;
  arenaReset 还会重置共享游标覆盖对方数据。
- v2 把位图 CAS 下沉到插件模板,正确但把分配器实现泄漏进插件运行时,
  且插件必须与内核保持位图布局同步。

新设计(用户明确的架构约束):内核全权管理共享内存,插件通过
syscall 风格 RPC 申请/归还,内核返回偏移与大小。分配器只存在于内核
进程内,一把 sync.Mutex 即可。共享内存是内部实现,不对插件开发者
暴露——SDK 公开 API 仍是普通字符串/Map。

主要改动:
- arena.go: 定长槽 + 位图,Alloc/Put/Read/Free/ReclaimOwner,内核独占;
  槽头记录 owner,Free 校验归属;payload 超槽容量退回内联 RPC
- protocol.go: 新增 arena.alloc / arena.free(CapCore)
- capability.go: 登记两个新 method(checkAllMethodsClassified 要求)
- corehandler.go: 实现 arena.alloc/free;Cleaner 改为内核预分配
  请求槽 + 响应槽(插件完全不分配)
- plugin.go: Start 领 ownerID;handleExit 调 ReclaimOwner 回收残留槽
- unified.go: 移除坏的 bump 分配器;arena 基址 8 字节对齐
- 删除 toollane.go: ring 状态机是死代码(从未接线),且把 RPC 已有的
  请求 ID 关联/错误传递/ctx 取消重新实现了一遍。控制面保留 RPC,
  只把 payload 搬进共享槽。
- 测试: TestArena_*(归属/回收/并发唯一/耗尽/超限/非法引用/布局)+
  TestPlugin_ArenaAllocFreeAcrossProcess(真进程申请→写入→随业务
  RPC 回传→归还→内核读回一致且池归零)
2026-09-10 17:56:50 +08:00

278 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.goprocCore 用命名字段持有内核 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,
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(), ", "))
}