mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-05 16:57:32 +00:00
## 起因(生产故障)
A2A 分诊助手(驻留子)把 terminal_create 当一次性命令执行器:
terminal_create(command='cd /usr/lib/node_modules/@deepseek-ai && ls -d */ | head -60 && …')
→ read /dev/ptmx: input/output error
→ 再开 term_7、term_8、term_9 …
链路拆开四层:
① cmd 插件 auto_start=false ⇒ 工具表里没有 cmd_run
② 需要 cmd-tools skill 说明"先 plugin_start cmd"
③ 但 skill 索引没进那个 agent 的提示词
④ 因为 skillIndex 只是根 agent 的字段、由 bootstrap 手工接线
④ 才是总根。而它之所以必须手工接线:agent/core 读不到内核的共享数据。
## 环的精确形状(实测,非推测)
kernelboot 因为持有 KernelState 而依赖 19 个业务包:
kernelboot ──▶ agent/core ──▶ …
──▶ plugin
──▶ security
──▶ memory / knowledge / text / coldcontext / …
而 kernelboot.go / scheduler.go / funcmodule.go / syncbus.go / lifecycle.go
**一行业务代码都不需要** —— 依赖全在 kernelstate.go 这一个数据文件里。
后果是四个底层包各自造通道:
core → Agent 字段(每种 agent 构造点都要记得传)
plugin → 12 个 SetXxx
sdk → SDKConfig 传参
security → 包级单例 Global() / RuntimeGlobal() / ConfigCodec()
四条通道是同一个根因的四个表现。
## 本次做的事
### 1. 抽出 internal/kstate(断环)
kernelstate.go / syncbus.go / lifecycle.go 搬出 kernelboot:
internal/kstate 纯数据 + 唯一挂载点(叶子,谁都能 import)
internal/kernelboot 装配引擎,零业务包
挂载点 API:Mount / Get(nil 安全)/ MountForTest(返回 restore,并行测试隔离)。
验证:core 现在能 import kstate(此前 import cycle not allowed)。
### 2. StageHost / Agent 字段存 any
它们是 core 自己的类型(core 是环的一极,这是真环)。按用户裁定的方向:
> 是其他包在kstate挂共享内容啊
存 any,消费方断言回具体类型 —— 与本仓既有的 PluginCatalog /
PluginScheduler / UpdateCapabilities 同型。
★ 其余字段保持**具体类型**,没有一律改 any。判据是"它会不会反向依赖
agent/core":实测 plugin / security / sdk 都不依赖 core,所以它们作为
具体类型不构成环,而类型化字段让"拼错/取错类型"是编译错误(本结构体的
设计原则)。
### 3. 插件声明式系统提示词段
SDK 新增 SystemPromptAPI(见 submodule 提交)。内核侧:
kstate.State.PromptSections *promptsec.Registry ← 唯一事实来源
Agent.renderSystemPrompt() 每轮重建时读它
skillmgr 从"实现 SkillIndexProvider 让内核来问"改为"声明自己的段":
DeclarePromptSection{Section:"skillmgr.skills", Order:100, Volatile:true}
并挂到全部变更点(启动全扫后、移交事件后、load/unload/create/install/
enable/disable 后、Stop 时撤销)。
### 4. 内核自身的提示词段也走同一管理器
按用户裁定「把现在内核所有 systemprompt 的相关注入都接入到这个统一的
systemprompt 管理器上」,把 buildSystemPrompt 里硬拼的十几块
(人格卡/任务提示词/记忆说明/输出规则/工具目录…)改为声明式:
kernelSections() 当轮计算 → 与插件段按 Order 合并渲染。
顺序由 Order 常量决定,改结构变成调常量而不是挪代码块。
## 判据(均经变异验证)
TestCoreCanImportKstate 真编译探针;环重现则红(实测)
TestKstateDoesNotDependOnCore 静态版,指向具体字段
TestKstateIsLeaf kstate 不得反依赖上层
TestRTChildSeesPromptSections 子 agent 能看到插件段
TestResidentSeesPromptSections 生产实际中招的那一类;变异后红(实测)
TestRootAgentSeesPromptSections 确认没改坏根路径
TestNoPromptSectionsMount 未装配时不 panic、不凭空出现段
TestDeclareTwiceRendersOnce ★ 回应"会不会多份":声明 5 次渲染 1 份
TestSectionRemovalTakesEffect Stop 后段消失
TestRuntimeUpdateVisibleToExistingAgent 同一实例下一轮看到新内容
TestSectionOrderIsRespected Order 生效且稳定
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l internal/ cmd/ pkg/ 干净
go test ./... -count=1 63 包全过,0 FAIL
干净检出(git worktree) build + 4 包测试全绿
## 未做(见 kernelplan.md)
T3 security 三单例收敛(含 Store 复用 Codec 的惰性迁移)
T5 plugin Setter 收敛
T6 防腐判据(部分已随本次落地)
153 lines
5.3 KiB
Go
153 lines
5.3 KiB
Go
// Package device 是 device 模块:**工具数据面的统一入口**。
|
||
//
|
||
// ## 定位(用户裁定 2026-10-26)
|
||
//
|
||
// > toolcall 部分作为一个完整的 device 子模块,管理全部的 tool、inputch 和
|
||
// > outputch。
|
||
//
|
||
// 第一阶段(本文件)先建立**正式模块 + 数据面对象 + KernelState 挂载点**,
|
||
// 工具执行仍委托 agent —— 行为零变化,但边界成立、安全检查有唯一落点。
|
||
// 第二阶段再把 tooldefs/toolcall 的实现迁进来(见 plan.md §七)。
|
||
//
|
||
// 为什么不一锅端:搬 2000+ 行工具实现的同时还有 1000+ 判据在跑,
|
||
// 任何一处不一致都无从定位。先把**边界与闸门**立起来,再搬实现。
|
||
//
|
||
// ## 分层位置
|
||
//
|
||
// config → security → memory/knowledge → stagehost → agent → device → pluginhost
|
||
//
|
||
// ## 安全纪律
|
||
//
|
||
// device 不实现安全逻辑,也不持有 raw security.Store:
|
||
// 只通过 ks.Security 的 Runtime 过境。任何"顺手直接调 store"的写法都是
|
||
// 绕过闸门(判据见 internal/security/gate_test.go)。
|
||
package device
|
||
|
||
import (
|
||
"fmt"
|
||
"log"
|
||
"sort"
|
||
|
||
"github.com/JianFeeeee/HologramAgent/internal/agent/api"
|
||
"github.com/JianFeeeee/HologramAgent/internal/events"
|
||
"github.com/JianFeeeee/HologramAgent/internal/kernelmod"
|
||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||
sec "github.com/JianFeeeee/HologramAgent/internal/security"
|
||
)
|
||
|
||
// Module 提供设备/工具数据面。
|
||
type Module struct{}
|
||
|
||
func (Module) Name() string { return "device" }
|
||
|
||
func (Module) Requires() []string {
|
||
return []string{kernelmod.FacAgent, kernelmod.FacSecurity}
|
||
}
|
||
|
||
func (Module) Provides() []string { return []string{kernelmod.FacDevice} }
|
||
|
||
func (Module) Init(ks *kstate.State) error {
|
||
// Agent 在挂载点里存 any(kstate 不能依赖 agent/core,否则成环)。
|
||
// 这里断言成**最小接口**而不是具体类型:device 只需要"能挂数据面"。
|
||
host, ok := ks.Agent.(api.DeviceMounter)
|
||
if !ok || host == nil {
|
||
return fmt.Errorf("device module: ks.Agent is not a DeviceMounter (requires %s)", kernelmod.FacAgent)
|
||
}
|
||
if ks.Security == nil {
|
||
// 安全模块必须已装配(配置关闭时是 Disabled Runtime,**不是** nil)。
|
||
// nil 表示装配顺序错了 —— 那会让本模块"看起来装好了但没有闸门"。
|
||
return fmt.Errorf("device module: ks.Security is nil - the security module must be " +
|
||
"assembled first (when disabled it installs a Disabled Runtime, not nil)")
|
||
}
|
||
|
||
// ExecuteTool 面:ToolHost 与 DeviceMounter 是两个不同用途的接口
|
||
// (执行 vs 装配),此处两个断言都要成功。
|
||
toolHost, ok := ks.Agent.(api.ToolHost)
|
||
if !ok || toolHost == nil {
|
||
return fmt.Errorf("device module: ks.Agent is not a ToolHost (requires %s)", kernelmod.FacAgent)
|
||
}
|
||
rt := New(toolHost, ks.Security)
|
||
ks.Device = rt
|
||
host.MountDevice(rt)
|
||
|
||
if ks.EventBus != nil {
|
||
ks.EventBus.Publish(&events.Event{
|
||
Type: events.EventDeviceReady,
|
||
Source: "kernelmod/device",
|
||
Payload: map[string]interface{}{
|
||
"tools": rt.ToolCount(),
|
||
"secured": rt.Secured(),
|
||
},
|
||
})
|
||
}
|
||
log.Printf("[device] ready: %d tools, security=%v", rt.ToolCount(), rt.Secured())
|
||
return nil
|
||
}
|
||
|
||
func init() { kernelmod.Register(Module{}) }
|
||
|
||
// Runtime 是 device 数据面。
|
||
type Runtime struct {
|
||
host api.ToolHost
|
||
sec *sec.Runtime
|
||
}
|
||
|
||
// New 构造 device 数据面。
|
||
func New(host api.ToolHost, s *sec.Runtime) *Runtime {
|
||
return &Runtime{host: host, sec: s}
|
||
}
|
||
|
||
// Security 返回安全运行时(只读,供过境用;不是变量库本身)。
|
||
func (r *Runtime) Security() *sec.Runtime {
|
||
if r == nil {
|
||
return nil
|
||
}
|
||
return r.sec
|
||
}
|
||
|
||
// Secured 报告安全审查是否真的生效(状态面用)。
|
||
func (r *Runtime) Secured() bool { return r != nil && r.sec != nil && r.sec.Enabled() }
|
||
|
||
// Tools 返回全部工具定义(按名字排序,便于人读与稳定输出)。
|
||
func (r *Runtime) Tools() []api.ToolDefinition {
|
||
if r == nil || r.host == nil {
|
||
return nil
|
||
}
|
||
out := r.host.ToolDefinitions()
|
||
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
||
return out
|
||
}
|
||
|
||
// ToolCount 返回工具数。
|
||
func (r *Runtime) ToolCount() int { return len(r.Tools()) }
|
||
|
||
// Execute 执行一次工具调用,并对**返回值**做出向边界处理。
|
||
//
|
||
// ★ 这是 device 的执行入口,但**不是**安全审查的唯一位置:
|
||
// - 参数校验 / 工具授权 / 设备授权 / 超时 / panic 恢复 → ToolHost(agent/core)
|
||
// - 明文 → 别名(出向)、别名 → 明文(执行前回填) → agent 的 stage 管道
|
||
// - 返回值清洗(入向)→ 本函数与 agent 的 defer 双保险
|
||
//
|
||
// 为什么入向要在两处都做:agent 的 defer 覆盖全部 return 出口(含 panic),
|
||
// 而本函数覆盖"经 device 的调用"。少一处就可能有一条路径不洗。
|
||
func (r *Runtime) Execute(name string, args map[string]interface{}) (string, error) {
|
||
if r == nil || r.host == nil {
|
||
return "", fmt.Errorf("device: tool host is not mounted")
|
||
}
|
||
out, err := r.host.ExecuteTool(name, args)
|
||
if err != nil {
|
||
return out, err
|
||
}
|
||
if r.sec != nil {
|
||
cleaned, n := r.sec.ScrubInbound(sec.Inspection{
|
||
Direction: sec.DirectionInbound,
|
||
Stage: "tool_result",
|
||
Tool: name,
|
||
}, out)
|
||
if n > 0 {
|
||
return cleaned, nil
|
||
}
|
||
}
|
||
return out, nil
|
||
}
|