fix(kernel): 补上统一挂载点的生产接线 —— 上一版功能实际未生效

## 这个 commit 修的是我自己上一版的破绽

上一轮我报告"63 包全绿"。今天用户问「确定都可以正常工作」,
一查发现:**生产路径上根本没人调 kstate.Mount()**。

    $ grep -rn 'kstate.Mount(' --include='*.go' internal/ cmd/ | grep -v _test
    (空)

后果链:

    kstate.Get() 永远返回 nil
      ⇒ agent/core 的 renderSystemPrompt 走 `if ks != nil` 跳过分支
      ⇒ 插件声明的系统提示词段**一条都不渲染**
      ⇒ 模型看不到任何 skill 索引,只能凭工具名猜用法
      ⇒ 正是本次重构要修的那个生产故障(拿 terminal_create 当 cmd_run)

而且**日志里一个字都没有** —— 静默跳过,不是报错。

## 为什么测试全绿

因为测试自己调了 `MountForTest`:

    restore := kstate.MountForTest(&kstate.State{PromptSections: reg})
    t.Cleanup(restore)

判据把缺失的那根线补上了,所以永远绿。这正是本仓反复吃过的亏
(engineering-lessons §1.1)。

## 一共两个断点

  ① kernelboot.Run 没调 kstate.Mount(ks)
  ② sdk.SetSystemPromptAPI 生产上没人调
     ⇒ 插件的 s.SystemPrompt() 返回 nil
     ⇒ skillmgr.publishIndex() 对 nil 是静默 return(它不能 panic)

## 修复

  ① kernelboot.Run 装入挂载点(唯一入口)
  ② 补齐三段接线:
       plugin.Registry 加 promptSections 字段 + SetPromptSections
       kernelmod/pluginhost 从 ks.PromptSections 注入进去
       internal/sdk 收到后调 SetSystemPromptAPI(promptSectionAdapter)

## 新增判据:**不自己 Mount**,只读 Run 之后的状态

  TestKstateIsMountedByRun             源码判据(那行必须在)
  TestPromptSectionsWiredToPlugins     三段接线逐个核对
  TestSharedDataInjectionsHaveProducers 通用:注入点必须有生产者
  TestRunActuallyMountsState           ★ 端到端

最后一条是关键:它先 `MountForTest(nil)` **清空**挂载点,再跑真实
`kernelboot.Run` + 真实模块登记表,然后从底层包视角(kstate.Get())
检查能不能看到东西。它不能靠"测试自己挂"蒙混过关。

变异验证(实测):
  · 删掉 kstate.Mount(ks) ⇒ 源码判据红 + 端到端判据红
    「kernelboot.Run 跑完了,但 kstate.Get() 仍是 nil」

## 验证

  go build ./...              PASS
  go vet ./...                PASS
  gofmt -l                    干净
  go test ./... -count=1      63 包全过,0 FAIL
  干净检出(git worktree)     build + 端到端判据全绿
  六环人工核对                 ①Mount ②Run ③产出 ④注入 ⑤sdk ⑥skillmgr 各 1 处

## 教训

"全绿"只在**判据不自己补线**时才是证据。凡是"被测方需要某个前置
装配、而判据自己提供了它"的判据,都必须再配一条"从零开始"的
端到端版本。
This commit is contained in:
JianFeeeee
2026-11-04 12:41:56 +08:00
parent 8cc8bca380
commit 01c7c0145e
5 changed files with 351 additions and 7 deletions

View File

@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/JianFeeeee/HologramAgent/internal/promptsec"
"github.com/JianFeeeee/HologramAgent/internal/security"
"log"
"os"
@ -81,6 +82,16 @@ type Registry struct {
// 只对声明了该能力的内置插件可见(目前是 WebUI)。
kernelCtl sdk.KernelControlAPI
// promptSections 是插件声明式系统提示词段的注册表(内核级共享)。
//
// ★ 为什么由 plugin 持有并传给 SDK:kstate 持有 *plugin.Registry
// (State.PluginRegistry),所以 plugin 不能 import kstate(会成环)。
// 方向只能是:plugin-host 模块从 ks.PromptSections 取出来注入。
//
// nil 合法:未装配时插件的 DeclarePromptSection 会拿到明确 error
//(而不是静默成功 —— 静默成功的症状是"插件以为声明了、模型看不到")。
promptSections *promptsec.Registry
// secretStore 是**变量库**(凭据别名),供 sdk.SecretsAPI 桥接。
//
// ★ 为什么是注入而不是 security.Global()(2026-11-04):
@ -361,6 +372,13 @@ func (r *Registry) SetSecurityRuntime(rt *security.Runtime) {
r.securityRT = rt
}
// SetPromptSections 注入插件声明式提示词段注册表(plugin-host 模块装配时调用)。
func (r *Registry) SetPromptSections(reg *promptsec.Registry) {
r.mu.Lock()
defer r.mu.Unlock()
r.promptSections = reg
}
// SetSecretStore 注入变量库(plugin-host 模块装配时调用)。
func (r *Registry) SetSecretStore(s *security.Store) {
r.mu.Lock()
@ -596,6 +614,7 @@ type sdkDeps struct {
kernelCtl sdk.KernelControlAPI
secretStore *security.Store
securityRT *security.Runtime
promptSections *promptsec.Registry
}
// snapshotDeps 在一次 RLock 内取走 buildSDK 需要的全部依赖。
@ -627,6 +646,7 @@ func (r *Registry) snapshotDeps() sdkDeps {
kernelCtl: r.kernelCtl,
secretStore: r.secretStore,
securityRT: r.securityRT,
promptSections: r.promptSections,
}
}
@ -803,6 +823,7 @@ func (r *Registry) buildSDK(name string) *sdk.PluginSDK {
// 内核未启用安全模块时为 nil ⇒ Available()==false(不是 nil API)。
Secrets: sdk.NewSecretsAPI(deps.secretStore, codecOf(deps.securityRT)),
SecurityRuntime: deps.securityRT,
PromptSections: deps.promptSections,
// ★ 策略声明面:插件**声明**档位(qq.owner / qq.guest…),
// 判定仍在内核 security.Runtime。Owner 由内核用真实插件名填,
// 插件无法伪造身份(见 internal/sdk/security_impl.go)。