- P0-5 护栏不再是空壳:挂 WithSensitiveInfo(阻断) + 长度上限(输入 262144 / 输出 65536 字符,依据=
组装完成的请求体量级与「不误伤正常对话」) + 关键词**仅在配置时**挂;正常对话有用例证明放行
- P0-11 上下文压缩默认启用:CONTEXT_MAX_TOKENS 默认 32768(=loop.DefaultMaxContextTokens,两常量由测试锁死),
0/负 = 关闭(行为退回接线前);长对话真压缩且摘要消息进请求首位、最近一轮保留
- P0-10 身份说明书(owner.yaml identityFile)注入人格上下文,并且**收窄为仅主人会话**:
· 新增最小钩子 loop.Options.PersonaKnowledgeFor(env):**按该轮会话环境**解析(不看提示词、不看会话 id),
块只进每轮视图、**不进 messages**(不进 checkpoint、不进记忆提取面)
· 开关 OWNER_IDENTITY_INJECTION:owner_only(默认) / always(仅自测) / off;非法值与读不动 → WARN + 回退默认(不静默)
· **顺手关掉第二条同型泄漏**:channelbridge 群聊参与度判定器的 PersonaPrompt 此前带身份正文 ⇒ 现改为纯人格指令
· 回归:缺文件/目录/未配置逐字节等于旧输出;静态指令与角色池 Instructions() 全不含正文;
文件里点名的陌生账号仍 IsOwner==false(身份知识绝不参与权限判定)
- 复核:build/vet 绿 + cmd/config/loop 包测试绿;四类断言(主人含 / **群聊与陌生人不含** / always·off 两档 /
回归不变)+ 4 条变异反证(含把判定改成恒真 → 提示词里真的出现身份说明书)均已还原
- 未做(登记):knowledge 块启动时读一次(改文件需重启);`.env.example` 的 OWNER_IDENTITY_INJECTION 行随后补;
`internal/user/owner.go` 注释与 design/01 §2.2、design/11 §0 待同步
63 lines
3.5 KiB
Go
63 lines
3.5 KiB
Go
package main
|
||
|
||
import "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/guardrail"
|
||
|
||
// 组合根的默认护栏策略(design/04 §5)。本条修的就是「护栏是空壳」:
|
||
// 此前 `guardrail.New()` 不带任何 Option,管线里一个检查都没有,
|
||
// 输入/输出两侧的 guardrail span 每次都只是走过场。
|
||
//
|
||
// 三条默认策略与它们的依据:
|
||
//
|
||
// 1. 敏感信息 = **阻断**档。API key / bearer token / 密码 / 私钥这类东西
|
||
// 一旦进了请求或回复就是既成事实的泄露,标记档(放行 + 记一笔)救不回来,
|
||
// 因此默认按阻断处理。
|
||
// 2. 长度上限 = 只拦病态载荷,不误伤正常对话(两个上限的取值依据见下方常量)。
|
||
// 3. 关键词表 = **只在配置提供时**挂。项目不内置任何词表:凭空猜的词只会
|
||
// 误伤正常对话,且没有部署方能为它的命中负责(见 config.GuardrailConfig)。
|
||
const (
|
||
// guardrailMaxInputChars 是请求侧的字符上限。这个检查跑在**组装完成的
|
||
// 请求**上(系统提示词 + 记忆块 + 整段对话 + 工具结果),不是单条用户
|
||
// 消息,因此上限必须按「整个上下文」来定:默认压缩预算 32K token
|
||
// 折算成字符约 65K(中文)~131K(拉丁),取 262144(256 Ki)字符约为
|
||
// 它的 2~4 倍 —— 正常对话(哪怕很长)永远碰不到,只有把几百 KB 的
|
||
// 整块文本直接灌进来这种病态输入才会被拦下。
|
||
guardrailMaxInputChars = 262144
|
||
// guardrailMaxOutputChars 是响应侧的字符上限。一条回复正常在几十到几百字,
|
||
// 长代码/长文也就几千字,65536(64 Ki)字符比正常回复高 2~3 个数量级,
|
||
// 因此不会截断任何正常输出,只拦失控的生成(例如无限复读)。这一点在主
|
||
// 流式路径上尤其重要:那里输出侧检查是异步旁路(只会审计,不挡已产出的
|
||
// 回复),但在静音/非流式路径(backthink、子代理)上否判决会替换答案,
|
||
// 所以上限必须留足余量。
|
||
guardrailMaxOutputChars = 65536
|
||
)
|
||
|
||
// buildGuardrailPipeline 组装循环与各 agent 共用的护栏管线(design/04 §5)。
|
||
// 它是组合根里唯一一处决定「护栏里到底有什么」的地方,因此测试可以直接
|
||
// 断言这个函数的产物,而不必启动整个运行时。
|
||
//
|
||
// 选项顺序即检查顺序(guardrail.New 按序应用),且先跑开销低、语义硬的
|
||
// 敏感信息检查:命中即返回该判决,长度与关键词检查不会再跑。
|
||
func (a *app) buildGuardrailPipeline() *guardrail.Pipeline {
|
||
opts := []guardrail.Option{
|
||
// ① 敏感信息外泄必须拦:阻断档。
|
||
guardrail.WithSensitiveInfo(guardrail.SensitiveBlock),
|
||
// ② 长度上限:0 会关掉那一侧,因此两侧都给上保守上限。
|
||
guardrail.WithLengthLimits(guardrailMaxInputChars, guardrailMaxOutputChars),
|
||
}
|
||
// ③ 关键词表仅在配置提供时挂:没有配置就不挂(KeywordGuard 空表本也
|
||
// 全放行,这里显式分支是为了让「没配置」在管线上就是没有这个检查)。
|
||
if keywords := a.guardrailKeywords(); len(keywords) > 0 {
|
||
opts = append(opts, guardrail.WithKeywords(keywords...))
|
||
}
|
||
return guardrail.New(opts...)
|
||
}
|
||
|
||
// guardrailKeywords 返回部署方声明的内容策略词表(GUARDRAIL_KEYWORDS);
|
||
// 未配置时返回 nil,管线里就不含关键词检查。
|
||
func (a *app) guardrailKeywords() []string {
|
||
if a.cfg == nil {
|
||
return nil
|
||
}
|
||
return a.cfg.Guardrail.Keywords
|
||
}
|