mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-02 15:27:33 +00:00
* feat(kernel): 模块化内核 + 编译期依赖图定序
把单体 init 序列拆成「模块自声明依赖 + 编译期生成依赖图 + 运行期只读图」。
## 为什么
原 cmd/homed/main.go 里 17 步 init 是**手写顺序**,依赖关系只存在于
注释和人脑里。实测踩到的坑(见 docs/zh/kernel-modularization-design.md):
· nlp.defaultParser 被 initONNXParser 写、被 distiller 读,两者相距 60 行,
只靠 ticker 10min 才没炸(隐式依赖,无声明)
· 加一个模块要人肉找"该插在哪",插错的表现是运行期 nil 解引用
· 关停是 defer ks.CloseAll(),而热更新路径 os.Exit(42) **跳过 defer**
⇒ docStore/distiller/textMem 的 flush 全部丢失(每次热更新都发生)
## 怎么改
- internal/kernelboot/:KernelState(一个结构体收全部包级共享变量,字段
nil 安全)、Module 接口、graphfile(依赖图编解码 + 与二进制 hash 对账)、
scheduler(按 BFS 层并行 Init)、syncbus(层边界写通知)、
lifecycle(Quiesce/Resume 可逆伪关机,区别于一次性 CloseAll)
- internal/kernelmod/<name>/:每个模块一个文件夹,Requires/Provides 自声明;
all/all.go 是唯一的 import 清单(Go 循环导入约束)
- cmd/homed-depgraph:编译期生成依赖图,Makefile 注入 digest
- 运行期只**读**图,不计算顺序(顺序是编译期决定的,源码里没有中央清单)
## 判据
kernelboot 8 条 + 图失败模式 7 条(全部拒绝并给准确错误)+ 模块目录数
与注册表一致性。变异测试 15/15 咬住,含:
· 删掉 AddCloser 的锁 → -race 报数据竞争
· Scheduler 同批跑去重 → 203 vs 200
· 图 hash 不匹配 → 拒绝启动
## 顺带修的缺陷
热更新路径漏调模块关闭 ⇒ 显式 stopKernelModules(),并用 AST 判据钉住
"必须在 os.Exit 之前"(字符串检查在本仓已骗过我三次)。
实测:冷启动 1630ms(5 层/17 模块,原 17 步串行);生产 22 插件全部建链。
* feat(security): 密钥防火墙 + 配置加密 + close 纪律
## 密钥防火墙
模型只见别名、不见明文;插件工具调用入口(StageHost.ExecuteTool)做入向
回填。三维能力:
· 出向:工具输出里的密钥被替换成 [[secret:name]] 别名后才进上下文
· 入向:模型传别名,入口处填回真值(插件完全无感知)
· 存储:config.db 里不存明文(0644 的库可读),敏感值独立 0600 文件
模式库「宁漏勿误伤」:宁可漏掉一次替换,也不把正常文本误当密钥。
## 配置加密
- internal/security/{codec,key,keyring,store,global}.go:AES-256-GCM,
enc:v1: 前缀,密钥轮换(OldKeys 解码 + 新钥加密)
- harden.go:数据目录权限收紧
- embedded.go:支持内嵌主密钥(可选,仍保留外部 key 文件路径)
- EncryptExistingSecrets / AuditSecrets:启动时幂等迁移 + 审计
## close 纪律(用户明确要求:只要调 close 就要有前置检查)
internal/safeclose:Safe / SafeFunc。全仓 37 处裸 close(chan) → 0。
关键教训(都写进注释和判据):
· `select { case <-ch: default: close(ch) }` **不是**并发安全的
(两个 goroutine 可同时命中 default)—— healthcheck 原先是这种写法
· `sync.Once` **不可重入**:X.Do(func(){ Safe(&X, ...) }) 会自锁,
症状是 go test 直接挂住(3 个包 600s 超时),栈里只有嵌套的 Once.Do
· map 里按请求持有的 channel(mcp/stdio.go、clawhubadapter/sidecar.go)
改成 *pendingRPC 包装,每通道一个 once
## 判据
security 全量 + TestNoBareCloseOutsideSafeclose(AST 级扫描,含注释剥离)
+ TestNoNestedSelfOnceDeadlock(正则修正:\w+ 不匹配点号,捕获组要含 '.')。
变异 2/2 咬住。全量 52 包 ok。
* feat(plugin+deploy): 插件三态生命周期、数据目录迁移、发行包修复
## 插件三态生命周期(用户要求:自动启动 / 启用但不自动启动 / 禁用)
- Manifest 新增 requires_facilities / auto_start(*bool,用于区分
"未声明"与"显式 false")
- plugin-host 模块(internal/kernelmod/pluginhost):用 kernelboot.Scheduler
按依赖时机拉起,**不再手写两阶段**
- 运行态停启 StopPlugin/StartPlugin **可逆**:不写 disabled_plugins
(不改用户决定)、不置 shuttingDown(之后还能加载)、会摘掉注册面
- 与 StopAll 的区别写进注释 —— StopAll 置 shuttingDown 后**永久无法加载**,
那是进程退出用的,误用于"暂停"就是关了就死
## 数据目录迁移(伪关机 → 迁移 → 唤醒)
- kernelboot/lifecycle.go:Quiesce/Resume(可逆,区别于一次性 CloseAll),
逆序暂停、逆序恢复、Pause 失败回滚
- kernelboot/migrate.go:BeginMigration/End 编排(先停插件 → 再停内核写者
→ 等在途归零)
- distiller/textmem 增 Pause/Resume 门(不是 cancel —— cancel 不可逆,
恢复要重建整个对象)
- 模块 migration:体检 + 生成可执行迁移脚本
★ 迁移脚本的三个实测教训(都写成了判据):
1. 无条件改 /etc/systemd/system ⇒ 演练把**生产** -data 改成了演练目录。
加 --dry-run(零改动)+ 强制校验 systemd 当前 -data == OLD
2. 本机没装 rsync,脚本在"服务已停、数据未搬"处硬失败 ⇒ cp -a 回退,
并明确警告不保硬链接/ACL(生产 plugindev 有大量 git 硬链接)
## 发行包修复(全部经实测确认)
1. package-linux.sh **完全漏打 depgraph.bin** ⇒ 内核 exit 1
("生产构建(内嵌 hash 非空)但读不到依赖图")
2. build_homed 不走 Makefile ⇒ 单独修 Makefile 无效,必须同时生成图+注入
3. build.sh 的 LDFLAGS 用**历史模块路径 gitcode.com** ⇒ -X 静默 no-op
⇒ --version 显示 unknown
4. Makefile -ldflags 用**单引号** ⇒ 注入了字面 $GRAPH_HASH
## ★ build.sh 的版本号必须校验(真实事故)
原 `VERSION="$(git describe --tags --dirty)"`。无 tag 分支上它返回**分支名**
`backup/pre-identity-rewrite-20261005-51-g03d87a4e-dirty`,被注入 meta.Version
⇒ 插件握手判据 MajorVersion() 把整串当主版本 ⇒ **22 个子进程插件全部
握手失败**("主版本不匹配")。内核活着、systemd active、日志每条都像
插件的问题 —— 实际是发行包带着非法版本号。
修法:git describe 只在形如 vX.Y.Z 时采用,否则回退 meta.Version(权威源),
并对最终值做形态守卫。deploy/packaging/version_guard_test.go 3 条判据,
变异 2/2 咬住。
## deb 的 systemd unit 改为 guard 模式
ExecStart=/usr/bin/homed --role=guard --data /var/lib/homeagent
(worker exit 42 → guard 重拉,不经过 systemd、不计失败)
含 RestartSteps/RestartMaxDelaySec/TimeoutStopSec + 硬化
(NoNewPrivileges/ProtectSystem/ProtectHome/DevicePolicy/ReadWritePaths)。
## 其他
- internal/plugin/proc:非法 JSON 帧的报错**可定位**(内容摘要 + 可能原因 +
修法)。生产实测 1038 条非法帧(插件往 stdout 打日志 = 污染协议通道)
- 示例插件 4 个 14 处 fmt.Printf → log.Printf
- hmapdev 模板同步修复;scaffold 判据剥离注释(模板注释里就含 fmt.Printf)
* chore: 收尾(主密钥轮换命令、IPC/知识库加固、SDK 对齐)
- cmd/homed-key-rotate:把 security 里的 RotateKey/RotateKeyFile/RekeySecrets
串成一条**可执行**的运维路径。没有它,E-1 就只是库里的能力而不是
"出事时能做的事",而轮换的价值恰恰在于能执行。
- cmd/homed:kmod_runtime(模块装配入口)、plugins_scheduled(failback
白名单)、memcandidate、bootstrap_order_test / shutdown_flush_test
- internal/ipc、internal/devicebridge、internal/knowledge:配套加固
- third_party/homeagent-sdk/sdk:PluginMgrAPI 增三态能力(mgrStop/mgrStart/
mgrIsRuntimeStopped)+ settings 对齐
判据:bootstrap_order_test(启动顺序不依赖手写序列)、
shutdown_flush_test(停机的两条路径必须对称 —— exit 42 会跳过 defer,
热更新路径下模块关闭/flush 曾全部丢失)。
* test(security): 测试夹具改用不可误认的占位值(修 push protection 拦截)
GitHub secret scanning 拦住了推送:三处测试夹具用了"看起来像真密钥"的
字面量:
internal/security/prefilter_test.go:39
internal/security/store_test.go:177
internal/security/survey_test.go:20
它们全是**假密钥**(`AKIAIOSFODNN7EXAMPLE` 是 AWS 官方文档的标准示例,
其余是 `abcdefghij...` 这类可读占位符),但扫描器无法区分 —— 它只看形态。
★ 不绕过(不用 unblock URL、不加 allowlist):直接改夹具。
原因:一旦允许"假密钥可以通过",真密钥混进来时就没有最后一道门了。
改法:保留**模式库匹配所需的结构**,把可识别内容换成 FAKE:
glpat-abcdefghijklmnopqrst → glpat-FAKE-TEST-TOKEN-NOT-REAL
sk-abcdefghij...012345 → sk-FAKE-TEST-KEY-NOT-REAL-0000000
AKIAIOSFODNN7EXAMPLE → AKIA0000FAKE0000TEST (保持 20 字符)
ASIAIOSFODNN7EXAMPLE → ASIA0000FAKE0000TEST
xoxb-123456789012-abcdefghijklmnop → xoxb-FAKE-TEST-TOKEN-NOT-REAL
★ 中途踩到一次:AWS 键的模式是 `\b(AKIA|ASIA)[0-9A-Z]{16}\b`(共 20 字符),
我第一次替换成 19 字符的 `AKIAFAKETESTKEY0000` ⇒ 模式匹配不上 ⇒
TestSurvey_WhatIsCaught / TestRecognizesCommonShapes /
TestPrefilterNeverBlocksRealMatches 三条当场变红。
判据正确地拒绝了"为了过扫描器而削弱覆盖"。
顺带核实:全仓 grep 无任何真实密钥泄漏。
* fix(packaging): package-linux.sh 的版本解析与 build.sh 统一
上一版只修了 build.sh,漏了 package-linux.sh —— 它仍只看 git describe:
VERSION="${VERSION:-$(git describe --tags --dirty)}"
无 tag 分支上 git describe 返回**分支名**
(backup/pre-identity-rewrite-20261005-56-g98850e1),守卫会正确拒绝,
但要求人**手动传 VERSION** 才能打包。而 build.sh 早已能自动回退 meta.Version
⇒ 同一个版本,走 build.sh 打得出来、走 package-linux.sh 打不出来。
危害不只是"要手动传参":运维为了跑通会去掉守卫,而分支名一旦进
meta.Version 就是**插件协议主版本**的来源 ⇒ 22 个子进程插件全部握手失败
(这是真实发生过的事故,见 3efa4e5)。
修法:两个脚本用同一套规则(git describe 仅在 vX.Y.Z 形态时采用,
否则回退 meta.Version),并对最终值做形态守卫。
判据:TestBothPackagingScriptsShareVersionRules —— 逐个读两个脚本,
要求都能从 meta/meta.go 取权威版本、都校验 git describe 形态。
变异(把 package-linux.sh 改回旧规则)咬住并准确指出是哪个脚本。
实测:不传 VERSION 跑 package-linux.sh,输出
[PKG] warn: git describe 给出非版本串(...),改用 meta.Version=1.4.0
Version: 1.4.0
* fix(test): 跨仓判据在 CI 上应 skip 而非 fail(修 PR 双红)
CI 上 "Go build / vet / test" 与 "Build linux/amd64" 双红:
--- FAIL: TestHmapdevTemplateImplementsAllPluginMgrMethods
bridge_sync_test.go:170: 读模板: open
../../../third_party/homeagent-sdk/tools/hmapdev/templates/proc_main.go.tmpl:
no such file or directory
## 根因:判据依赖了一个**被 gitignore 的路径**
`.gitignore:33` 有 `third_party/homeagent-sdk/tools/`(SDK 仓自治,本仓不跟踪)。
于是:
· 本机(完整 SDK 检出)有该文件 ⇒ 判据通过
· CI(只 checkout 本仓)没有该文件 ⇒ Fatalf ⇒ **必红**
我提交时只在本机跑测试、没看 CI,所以这个问题拖到 PR 才暴露。
## 修法
文件缺失时 `t.Skipf` 而不是 `t.Fatalf`。
★ 这不是"放弃检查":
· 跨仓一致性由 **SDK 仓自己的判据**守着
(tools/hmapdev/scaffold_stdout_test.go 等),那里才是模板的真实归属地
· 本判据在本机开发时**仍然生效**(已验证:有文件时 PASS,缺文件时 SKIP)
## 验证
· 本机(有文件) → PASS(判据真跑)
· 模拟 CI(移走 tools/)→ SKIP(不再 fail)
· 全量 53 包 ok / 0 fail
## 顺带
同目录还有 e2e_template_test.go 也读 tools/,但它是 `//go:build` 门控的
e2e 判据,CI 默认不跑,无需处理。
---------
Co-authored-by: JianFeeeee <JianFeeeee@users.noreply.gitcode.com>
373 lines
17 KiB
Go
373 lines
17 KiB
Go
package proc
|
||
|
||
import (
|
||
"encoding/json"
|
||
"strings"
|
||
)
|
||
|
||
// RPC 协议定义:控制面(§3.2 method id 平移为 method 名)。
|
||
//
|
||
// 帧格式:**换行分隔的 JSON**(NDJSON),双向复用同一对 stdio 管道。
|
||
// 内核 → 插件 stdin :请求 / 响应
|
||
// 插件 → 内核 stdout:请求 / 响应
|
||
//
|
||
// 为什么不用 length-prefixed 二进制帧:工具调用结果中位数仅 93B(§2.5),
|
||
// JSON 序列化 3-8 µs 对比 LLM 单轮 2-8 秒占 0.0001%,可读性与可调试性更值。
|
||
// 大 payload(多媒体二进制)走共享内存 arena,不进 RPC 帧(§3.3 实验 10:18-22x)。
|
||
|
||
// 协议兼容性判据:**大版本号唯一**。
|
||
//
|
||
// 不自定协议号。插件的兼容性由双方各自的版本号表态:内核侧 meta.Version、
|
||
// 插件侧 sdk_version(SDK 版本跟随内核中版本,patch 恒为 .0)。
|
||
// 版本号唯一事实源就是这两个 meta.Version(见 docs/zh/ci-cd-runbook.md)。
|
||
//
|
||
// 判据是**主版本相同**:
|
||
// - 1.x ↔ 1.y:兼容。1.0.0 之后全是纯追加,不调新面就不受影响。
|
||
// - 跨主版本(如 1.x → 2.x):拒绝建链,提示用配套 hmapdev 重编。
|
||
// 主版本跃迁正是**破坏性变更**的定义(1.0.0 从 C ABI 改子进程即属此列),
|
||
// 所以它天然是正确的分界,不需要另一个号来复述这件事。
|
||
//
|
||
// 布局一致性另由 stageFieldCount(描述符个数)把关:它是结构的派生事实,
|
||
// 也不是版本号。两者错配都在握手上显式失败,不半兼容运行。
|
||
|
||
// MajorVersion 取版本号的主版本段("1.4.0" → "1","" → "")。
|
||
func MajorVersion(v string) string {
|
||
v = strings.TrimPrefix(strings.TrimSpace(v), "v")
|
||
if i := strings.IndexByte(v, '.'); i >= 0 {
|
||
return v[:i]
|
||
}
|
||
return v
|
||
}
|
||
|
||
// Direction 无需显式字段:靠 Method 是否为空区分请求与响应
|
||
// (与 clawhubadapter/sidecar 的成熟做法一致)。
|
||
|
||
// Request 是一次 RPC 调用。
|
||
//
|
||
// ID 语义:
|
||
// - ID > 0 :需要响应,调用方在 pending 表等待
|
||
// - ID == 0 :通知(fire-and-forget),被调方不得回响应
|
||
//
|
||
// 通知用于事件投递等不关心结果的路径(§2.4 约束 B:内核发通知绝不等待消费者)。
|
||
type Request struct {
|
||
ID uint64 `json:"id,omitempty"`
|
||
Method string `json:"method"`
|
||
Params json.RawMessage `json:"params,omitempty"`
|
||
}
|
||
|
||
// Response 是对 Request 的应答。Error 非空表示失败。
|
||
type Response struct {
|
||
ID uint64 `json:"id"`
|
||
Result json.RawMessage `json:"result,omitempty"`
|
||
Error string `json:"error,omitempty"`
|
||
}
|
||
|
||
// ---- kernel → plugin(内核调用插件;工具执行、Cleaner、stage、输出等)----
|
||
const (
|
||
// MethodPluginInit 传插件名与配置,插件构造实例但不启动。
|
||
MethodPluginInit = "plugin.init"
|
||
// MethodPluginStart 插件注册工具/阶段/通道(其间会反向发起大量 core.* 调用)。
|
||
MethodPluginStart = "plugin.start"
|
||
// MethodPluginStop 优雅停止:插件侧先跑 RunStopHandlers 再 Stop()。
|
||
MethodPluginStop = "plugin.stop"
|
||
// MethodToolInvoke 执行插件工具。
|
||
MethodToolInvoke = "tool.invoke"
|
||
// MethodCleanerInvoke 在插件进程内执行工具或通道声明的 Cleaner。
|
||
// Cleaner 是函数,不能随注册请求 JSON 序列化;内核保留 RPC 回调闭包,
|
||
// 需要参与向量化/蒸馏时把原文送回插件执行真正的 Cleaner。
|
||
MethodCleanerInvoke = "cleaner.invoke"
|
||
// MethodStageInvoke 执行阶段处理器。数据经共享段传递,参数只带阶段名与段世代号。
|
||
MethodStageInvoke = "stage.invoke"
|
||
// MethodOutputInvoke 经插件输出通道发送。
|
||
MethodOutputInvoke = "output.invoke"
|
||
// MethodArenaAlloc 向内核申请一块共享内存(返回偏移与大小)。
|
||
// MethodArenaFree 通知内核回收先前申请的共享内存。
|
||
//
|
||
// 这两个是**内部传输层接口**,不由插件开发者直接使用:共享内存是
|
||
// 内核的内部实现,公开 SDK 仍是普通字符串/Map,模板运行时按
|
||
// payload 大小自动选择内联 JSON 还是共享槽。
|
||
MethodArenaAlloc = "arena.alloc"
|
||
MethodArenaFree = "arena.free"
|
||
// MethodHandshake 建链首帧:交换协议版本、SDK 版本、共享段规格。
|
||
MethodHandshake = "handshake"
|
||
)
|
||
|
||
// ---- plugin → kernel(51 个 method id 平移,§3.2)----
|
||
//
|
||
// 编号本身扔掉:不再维护"下一个可用 id 是 52",加能力不用改两边常量表,
|
||
// 也不再出现 47 夹在 7 和 8 之间的历史痕迹。
|
||
const (
|
||
// 注册面(原 case 1/2/3/4/46)
|
||
MethodToolRegister = "tool.register" // 1 CORE_REGISTER_TOOL
|
||
MethodStageRegister = "stage.register" // 2 CORE_REGISTER_STAGE
|
||
MethodOutputRegister = "output.register" // 3 CORE_REGISTER_OUTPUT_CH
|
||
MethodAPIRegister = "api.register" // 4 CORE_REGISTER_PLUGIN_API
|
||
MethodInputRegister = "input.register" // 46 CORE_REGISTER_INPUT_CH
|
||
|
||
// IO 注入(原 case 5/6/7/47)
|
||
MethodIOInjectText = "io.injectText" // 5 CORE_INJECT_TEXT
|
||
MethodIOInjectInterrupt = "io.injectInterrupt" // 6 CORE_INJECT_INTERRUPT_TEXT
|
||
MethodIOInjectTextNoMem = "io.injectTextNoMem" // 7 CORE_INJECT_TEXT_NO_MEMORY
|
||
MethodIOInjectSync = "io.injectInputSync" // 47 CORE_INJECT_INPUT_SYNC
|
||
// 带媒体的注入:blocks 随参数 JSON 一并过来,内核侧转成
|
||
// payload["media_blocks"],由 resolveInput 归一进统一输入主干。
|
||
// 与 SetToolBlocks 的区别:这三个是「主动发起一轮带图的对话」,
|
||
// 后者是「工具返回值里带图」,只能在工具调用内部用。
|
||
MethodIOInjectMedia = "io.injectMedia"
|
||
MethodIOInjectMediaSync = "io.injectMediaSync"
|
||
MethodIOInjectInterruptMedia = "io.injectInterruptMedia"
|
||
// MethodIOSetToolBlocks 多模态注入——今日 C ABI 侧是空实现(§1.4),
|
||
// 子进程下二进制落 arena、描述符回传,首次真正可用。
|
||
MethodIOSetToolBlocks = "io.setToolBlocks"
|
||
|
||
// 生命周期(原 case 8)
|
||
MethodLifecycleAutoRestart = "lifecycle.autoRestart" // 8 CORE_SET_AUTO_RESTART
|
||
|
||
// 图记忆(原 case 9/10/11/12/13)
|
||
MethodMemoryRecall = "memory.recall" // 9
|
||
MethodMemoryCommit = "memory.commit" // 10
|
||
MethodMemoryIntrospect = "memory.introspect" // 11
|
||
MethodMemoryMerge = "memory.merge" // 12
|
||
MethodMemoryPurge = "memory.purge" // 13
|
||
|
||
// 文档记忆(原 case 14/32/33/34)
|
||
MethodDocQuery = "doc.query" // 14
|
||
MethodDocInsert = "doc.insert" // 32
|
||
MethodDocRemove = "doc.remove" // 33
|
||
MethodDocStats = "doc.stats" // 34
|
||
// MethodDocInsertMedia 写入文档并关联媒体(附件带 data 则落盘去重,
|
||
// 只带 digest 则引用已有内容)。
|
||
MethodDocInsertMedia = "doc.insertWithMedia"
|
||
|
||
// 知识库(原 case 15/35/36)
|
||
MethodKnowledgeSearch = "knowledge.search" // 15
|
||
MethodKnowledgeAdd = "knowledge.add" // 35
|
||
MethodKnowledgeList = "knowledge.list" // 36
|
||
// MethodKnowledgeAddMedia 写入知识并关联媒体(与 doc.insertWithMedia
|
||
// 同形)。媒体成为一等节点参与跨模态召回;未接入多模态空间时与
|
||
// knowledge.add 等价。
|
||
MethodKnowledgeAddMedia = "knowledge.addWithMedia"
|
||
|
||
// 文本记忆(原 case 41)
|
||
MethodTextMemoryAppend = "textmemory.append" // 41
|
||
|
||
// 设置(原 case 16/17/18/26/27/28/29/30/31/42/43/44/45/51)
|
||
MethodSettingsGet = "settings.get" // 16
|
||
MethodSettingsSet = "settings.set" // 17
|
||
MethodSettingsRegisterDef = "settings.registerDef" // 18
|
||
MethodSettingsGetCore = "settings.getCore" // 26
|
||
MethodSettingsSetCore = "settings.setCore" // 27
|
||
MethodSettingsListCore = "settings.listCore" // 28
|
||
MethodSettingsGetPlugin = "settings.getPlugin" // 29
|
||
MethodSettingsSetPlugin = "settings.setPlugin" // 30
|
||
MethodSettingsListPlugin = "settings.listPlugin" // 31
|
||
MethodSettingsList = "settings.list" // 42
|
||
MethodSettingsDefs = "settings.defs" // 43
|
||
MethodSettingsDump = "settings.dump" // 44
|
||
MethodSettingsPlugins = "settings.plugins" // 45
|
||
MethodSettingsDataDir = "settings.dataDir" // 51
|
||
// 密文读写(强制加密,不看键名)—— 与 settings.set/get 的区别是
|
||
// 这两个**必然**走加密路径,用于插件用中性键名存凭据的场景。
|
||
MethodSettingsSetSecret = "settings.setSecret" // 52
|
||
MethodSettingsGetSecret = "settings.getSecret" // 53
|
||
|
||
// 变量库(全局共享,非 per-plugin;见 internal/security 与 corehandler_secrets.go)。
|
||
//
|
||
// 与 settings 的分工是本质的:settings 是**每个插件自己的**配置,
|
||
// secrets 是**全进程共享**的变量库单例(主/子/驻留子 agent 与全部插件共用)。
|
||
MethodSecretsAvailable = "secrets.available" // 32
|
||
MethodSecretsStore = "secrets.store" // 33
|
||
MethodSecretsList = "secrets.list" // 34
|
||
MethodSecretsForget = "secrets.forget" // 35
|
||
MethodSecretsFill = "secrets.fill" // 36
|
||
MethodSecretsEncrypt = "secrets.encrypt" // 46
|
||
MethodSecretsDecrypt = "secrets.decrypt" // 47
|
||
|
||
// LLM 源(原 case 19/20/37)
|
||
MethodLLMListSources = "llm.listSources" // 19
|
||
MethodLLMSetSource = "llm.setSource" // 20
|
||
MethodLLMCurrentSource = "llm.currentSource" // 37
|
||
|
||
// 社交图(只读,原 case 21/22/38/39/40)
|
||
MethodSocialGetPerson = "social.getPerson" // 21
|
||
MethodSocialGetNetwork = "social.getNetwork" // 22
|
||
MethodSocialGetTrait = "social.getTrait" // 38
|
||
MethodSocialGetRelation = "social.getRelations" // 39
|
||
MethodSocialListPersons = "social.listPersons" // 40
|
||
|
||
// 事件(原 case 23/24 —— 今日均为空实现「给不了」,
|
||
// 子进程下经事件环 + eventfd 首次真正可用,见 §3.6/§3.8)
|
||
MethodEventsSubscribe = "events.subscribe" // 23
|
||
MethodEventsUnsubscribe = "events.unsubscribe" // 24
|
||
|
||
// 插件管理(原 case 48/49/50)
|
||
MethodPluginReloadOne = "plugin.reloadOne" // 48
|
||
MethodPluginListLoaded = "plugin.listLoaded" // 49
|
||
MethodPluginIsDisabled = "plugin.isDisabled" // 50
|
||
// ---- 运行态启停(设计 §2.6.7):与 isDisabled(持久态)分开 ----
|
||
//
|
||
// ★ 为何必须桥接:子进程插件走本桥,未在此登记的方法会被
|
||
// corehandler 直接拒("未知 method")⇒ SDK 上加了方法但插件**调不通**。
|
||
// 同进程插件(.so)直接调 SDK,不受影响 —— 这个不对称容易漏。
|
||
MethodPluginMgrStop = "plugin.mgrStop" // 51
|
||
MethodPluginMgrStart = "plugin.mgrStart" // 52
|
||
MethodPluginMgrIsRuntimeStopped = "plugin.mgrIsRuntimeStopped" // 53
|
||
|
||
// 共享段锁仲裁(新增,无对应 method id —— C ABI 下不存在跨进程锁概念)
|
||
MethodStageLock = "stage.lock"
|
||
MethodStageUnlock = "stage.unlock"
|
||
)
|
||
|
||
// 原 case 25(CORE_FREE_STRING)无对应 RPC method:
|
||
// C ABI 下需要显式释放跨边界字符串,进程模型下由各自 GC 管理,概念消失。
|
||
|
||
// HandshakeParams 是内核 → 插件的建链首帧:告知内核侧规格。
|
||
type HandshakeParams struct {
|
||
CoreVersion string `json:"core_version"` // 内核版本(插件据此判主版本兼容)
|
||
PluginName string `json:"plugin_name"` // 内核分配的插件名
|
||
// FieldCount 是 StageContext 描述符个数。插件据此与自己的编译期
|
||
// 字段数比对:不匹配意味着标志位基址会错位,必须重编(而非静默错读)。
|
||
FieldCount uint32 `json:"field_count"`
|
||
ShmSize int `json:"shm_size"`
|
||
// EvtRingSize 是事件环段大小(0 表示不支持事件环)。插件据此 mmap fd 4。
|
||
EvtRingSize int `json:"evt_ring_size,omitempty"`
|
||
}
|
||
|
||
// HandshakeResult 是插件 → 内核的建链应答:上报自身信息。
|
||
type HandshakeResult struct {
|
||
// SDKVersion 是插件编译时链接的公开 SDK 版本。内核据此判主版本兼容。
|
||
SDKVersion string `json:"sdk_version"`
|
||
PluginName string `json:"plugin_name"`
|
||
PID int `json:"pid"`
|
||
}
|
||
|
||
// StageInvokeParams 是 stage.invoke 的参数。
|
||
//
|
||
// **注意:不含 StageContext 数据本身**——数据在共享段,此处只带定位信息。
|
||
// 这是共享内存数据面的意义:并发改写同一份状态,而非各持副本
|
||
// (副本模型实测 35.8~36.8% lost update,§8.4)。
|
||
type StageInvokeParams struct {
|
||
Stage string `json:"stage"`
|
||
// Seq 是内核写入共享段后的世代号,插件读到的 seq 应 >= 此值。
|
||
Seq uint64 `json:"seq"`
|
||
}
|
||
|
||
// StageInvokeResult 是插件执行 stage 后的应答。
|
||
type StageInvokeResult struct {
|
||
// DirtyFields 是插件实际写回共享段的字段数,0 表示只读插件。
|
||
// 内核据此判断是否需要重读共享段,也用于诊断"谁改了什么"。
|
||
DirtyFields int `json:"dirty_fields"`
|
||
// Seq 是插件写回后的世代号。
|
||
Seq uint64 `json:"seq"`
|
||
}
|
||
|
||
// ToolInvokeParams:工具调用(原 go_invoke_tool)。
|
||
//
|
||
// 按 **funccall 模型**,内核(caller)为每次调用标定一块内存帧交给插件
|
||
// (callee),插件在这块内存里工作:
|
||
//
|
||
// [0, ArgsLen) 参数 JSON
|
||
// [ArgsLen, Frame.Length) 结果区(内核预留的预算)
|
||
//
|
||
// 结果放得下就写在帧内;**不够用时插件才向内核申请扩容**(arena.alloc),
|
||
// 并在返回引用上打 sharedRefFlagExpand,内核据此单独归还扩容块。
|
||
//
|
||
// 参数永远在共享内存里,不存在“小 payload 走内联”的按大小分支。
|
||
//
|
||
// Args 仅剩给**直连 RPC 的调用方**(process/bench 测试不建 Host,拿不到
|
||
// 共享内存);内核的 invokeTool 始终走 Frame。
|
||
type ToolInvokeParams struct {
|
||
Name string `json:"name"`
|
||
Frame SharedRef `json:"frame,omitempty"`
|
||
ArgsLen uint32 `json:"args_len,omitempty"`
|
||
Args map[string]interface{} `json:"args,omitempty"` // 仅直连 RPC 调用方使用
|
||
}
|
||
|
||
type ToolInvokeResult struct {
|
||
ResultRef SharedRef `json:"result_ref,omitempty"`
|
||
Result interface{} `json:"result,omitempty"` // 仅直连 RPC 调用方使用
|
||
}
|
||
|
||
// CleanerInvokeParams / CleanerInvokeResult:跨进程计算层清洗。
|
||
// Scope 取 tool / input / output,Name 是工具名或通道名。
|
||
//
|
||
// 与工具调用用**同一个 funccall 帧模型**:
|
||
//
|
||
// [0, InputLen) 输入文本
|
||
// [InputLen, Frame.Length) 结果区(内核预留的预算)
|
||
//
|
||
// 结果放不下时插件申请扩容块,并在 TextRef 上打 sharedRefFlagExpand。
|
||
type CleanerInvokeParams struct {
|
||
Scope string `json:"scope"`
|
||
Name string `json:"name"`
|
||
Frame SharedRef `json:"frame,omitempty"`
|
||
InputLen uint32 `json:"input_len,omitempty"`
|
||
}
|
||
|
||
type CleanerInvokeResult struct {
|
||
TextRef SharedRef `json:"text_ref,omitempty"`
|
||
}
|
||
|
||
// 调用帧的结果预算。
|
||
//
|
||
// 内核按“参数长度 + 预算”标定帧;结果超出预算不是失败,插件会申请扩容块。
|
||
// 预算取 64KB:覆盖绝大多数工具结果,使常态调用完全免于第二次分配。
|
||
const (
|
||
toolResultBudget = 64 * 1024
|
||
cleanerResultBudget = 64 * 1024
|
||
)
|
||
|
||
const (
|
||
CleanerScopeTool = "tool"
|
||
CleanerScopeInput = "input"
|
||
CleanerScopeOutput = "output"
|
||
)
|
||
|
||
// OutputInvokeParams:输出通道发送(原 go_invoke_output)。
|
||
//
|
||
// 与 C ABI 路径的关键差异:**可同步等待真实结果**。
|
||
// C ABI 下因 cgo 不可嵌套,只能异步 fire-and-forget,导致 output_send
|
||
// 永远返回成功(§9.4,现网 2 次消息发不出而模型以为成功)。
|
||
// 进程模型下 RPC 天然可等应答,该缺陷从根上消失。
|
||
type OutputInvokeParams struct {
|
||
Channel string `json:"channel"`
|
||
// Frame 是调用帧:[0, ArgsLen) 是参数 JSON;[ArgsLen, Frame.Length) 是结果区。
|
||
// 与 ToolInvokeParams 同一 funccall 帧模型(§13.3)。没有帧时(直连 RPC
|
||
// 测试)回退到 Args。大 payload 不再爆 stdin/stdout 管道——这正是同步
|
||
// output_send 卡住的根因之一。
|
||
Frame SharedRef `json:"frame,omitempty"`
|
||
ArgsLen uint32 `json:"args_len,omitempty"`
|
||
Args map[string]interface{} `json:"args,omitempty"` // 仅直连 RPC 调用方使用
|
||
}
|
||
|
||
// OutputInvokeResult 与 ToolInvokeResult 同形:结果优先写进帧结果区,
|
||
// 放不下才申扩容块并打 sharedRefFlagExpand。标量响应(如 "ok")直接在
|
||
// Result 字段返回,不走共享内存。
|
||
type OutputInvokeResult struct {
|
||
ResultRef SharedRef `json:"result_ref,omitempty"`
|
||
Result interface{} `json:"result,omitempty"` // 仅直连 RPC 调用方使用
|
||
}
|
||
|
||
// ArenaAllocParams / ArenaAllocResult:插件向内核申请共享内存。
|
||
//
|
||
// 内核返回的 Ref.Length 是槽容量(可写上限),插件写入后自行把 Length
|
||
// 改成实际 payload 长度再随业务 RPC 回传。
|
||
type ArenaAllocParams struct {
|
||
Size uint32 `json:"size"`
|
||
}
|
||
|
||
type ArenaAllocResult struct {
|
||
Ref SharedRef `json:"ref"`
|
||
}
|
||
|
||
// ArenaFreeParams:插件通知内核回收共享内存。只带描述符,槽号在 Flags 里。
|
||
type ArenaFreeParams struct {
|
||
Ref SharedRef `json:"ref"`
|
||
}
|
||
|
||
// PluginInitParams:插件构造参数(原 case init_plugin)。
|
||
type PluginInitParams struct {
|
||
Name string `json:"name"`
|
||
Config map[string]interface{} `json:"config,omitempty"`
|
||
}
|