Files
Cyrene_v2/cmd/cyrene/wire_guardrail.go
T
AskaEth da3a386daf
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(cyrene,config,loop): 护栏/压缩/身份知识三条落地(P0-5/P0-10/P0-11)+ 身份注入收窄到主人会话
- 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 待同步
2026-10-04 14:30:26 +08:00

63 lines
3.5 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 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
}