P0-3 受限车道(群聊/陌生人)的系统框架行并列注入【公域呈现】 (publicPresencePrompt):它是呈现层投影(AGENTS §9 行为模式=呈现), 不是人格切换也不是权限门——不称第三方「亲爱的」等主人专属称呼、 不做只对主人的亲近动作、主人的住址/姓名/作息/行程/健康/家人/关系 即使记忆里读到也不外说;主人私聊与 Web 主会话不注入。 P0-4 纵深防御:第三方边界提示词与 loop 受限前导片段各补两条硬规则 (不用主人专属亲昵称呼指代第三方 + 不外说主人私事)。 P1-2 判定器去人格:模型输入只吃「角色名 + 偏好摘要」,整份人格指令 不再进模型(审计 2026-10-30 §2.4 的 reason 亲昵口吻即由此而来)。 P1-3 测试补真:受限期呈现块存在性 + 主人车道不注入 + 判定器输入/ 模型请求均不含人格正文;并纠正 memory_filter 的错误注释 (作用域 ≠ 敏感度,审计 §6 R1)。
154 lines
6.1 KiB
Go
154 lines
6.1 KiB
Go
package channelbridge
|
||
|
||
import (
|
||
"context"
|
||
"strings"
|
||
"testing"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/restrict"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||
)
|
||
|
||
// 本文件覆盖公域呈现块(审计 2026-10-30 §5.3 / P0-3)与第三方边界的
|
||
// 两条硬规则(P0-4)的**存在性与注入边界**:
|
||
//
|
||
// - 受限车道(群聊 / 陌生人私聊)的系统框架行必须带公域呈现块;
|
||
// - 主人私聊与 Web 主会话**不得**带(那里没有第三方,呈现档位就是原档);
|
||
// - 呈现块自己必须说清它是「呈现层投影」而不是人格切换(AGENTS §9)。
|
||
//
|
||
// 这些用例在删掉注入点时必红(删实现打红实测见提交说明)。
|
||
|
||
// restrictedGroupEnv / restrictedStrangerEnv 与渠道桥为受限会话解析出的
|
||
// 环境同形:受限、记忆只读、不绑定显式角色(design/22 §7-4)。
|
||
func restrictedGroupEnv() loop.SessionEnv {
|
||
return loop.SessionEnv{ActorEnv: restrict.ActorGroupMember, MemWritable: false}
|
||
}
|
||
|
||
func restrictedStrangerEnv() loop.SessionEnv {
|
||
return loop.SessionEnv{ActorEnv: restrict.ActorStranger, MemWritable: false}
|
||
}
|
||
|
||
// ownerPrivateEnv 是主人私聊 / Web 主会话的环境(非受限)。
|
||
func ownerPrivateEnv() loop.SessionEnv {
|
||
return loop.SessionEnv{ActorEnv: restrict.ActorOwner, MemWritable: true}
|
||
}
|
||
|
||
// systemFrameOf 返回消息列表里第一条系统行的正文;没有系统行时返回 ""。
|
||
func systemFrameOf(msgs []model.Message) string {
|
||
for _, m := range msgs {
|
||
if m.Role == model.RoleSystem {
|
||
return turnMessageText(m)
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// frameMessageOf 返回消息列表里第一条系统消息(用于断言它确实是一条)。
|
||
func frameMessageOf(msgs []model.Message) (model.Message, bool) {
|
||
for _, m := range msgs {
|
||
if m.Role == model.RoleSystem {
|
||
return m, true
|
||
}
|
||
}
|
||
return model.Message{}, false
|
||
}
|
||
|
||
func TestRestrictedLanesCarryPublicPresenceBlock(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
b := &Bridge{}
|
||
ctx := context.Background()
|
||
member := adapter.Actor{Role: adapter.RoleMember}
|
||
owner := adapter.Actor{Role: adapter.RoleOwner}
|
||
|
||
groupMsg := adapter.UnifiedMessage{
|
||
Platform: "kook", ChannelType: adapter.ChannelGroup, GroupID: "g1", GroupName: "翁法罗斯",
|
||
}
|
||
directMsg := adapter.UnifiedMessage{
|
||
Platform: "telegram", ChannelType: adapter.ChannelDirect, SenderID: "999",
|
||
}
|
||
webMsg := adapter.UnifiedMessage{Platform: "web", ChannelType: adapter.ChannelDirect}
|
||
|
||
cases := []struct {
|
||
name string
|
||
env loop.SessionEnv
|
||
actor adapter.Actor
|
||
msg adapter.UnifiedMessage
|
||
want bool
|
||
}{
|
||
{"群聊里的第三方", restrictedGroupEnv(), member, groupMsg, true},
|
||
{"陌生人私聊", restrictedStrangerEnv(), member, directMsg, true},
|
||
// 主人在群里同样跑受限车道:公域里没有「只对主人的那份亲近」,
|
||
// 呈现档位按会话而不是按对方的身份决定(呈现层,不动权限)。
|
||
{"主人在群里", restrictedGroupEnv(), owner, groupMsg, true},
|
||
{"主人私聊", ownerPrivateEnv(), owner, directMsg, false},
|
||
{"Web 主会话", ownerPrivateEnv(), owner, webMsg, false},
|
||
}
|
||
for _, tc := range cases {
|
||
tc := tc
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
t.Parallel()
|
||
msgs := b.buildLoopMessages(ctx, "hello", tc.env, tc.actor, "", tc.msg)
|
||
sys := systemFrameOf(msgs)
|
||
if got := strings.Contains(sys, publicPresencePrompt); got != tc.want {
|
||
t.Fatalf("公域呈现块存在 = %v, want %v;系统框架行 = %q", got, tc.want, sys)
|
||
}
|
||
// 呈现块与身份边界**并列**:受限车道缺任何一半都不算完整
|
||
// (边界管「谁是谁」,呈现块管「怎么称呼、能说什么」)。
|
||
if tc.want {
|
||
if !strings.Contains(sys, thirdPartyBoundaryPrompt) {
|
||
t.Fatalf("受限框架行缺第三方身份边界:%q", sys)
|
||
}
|
||
if got, ok := frameMessageOf(msgs); !ok || got.Role != model.RoleSystem {
|
||
t.Fatalf("受限框架行不是一条系统消息:%+v", got)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestPublicPresenceBlockIsPresentationalProjection 钉住呈现块的**定位**
|
||
// (AGENTS §9:人格 = 身份不限制能力;行为模式 = 呈现可切档):
|
||
// 它必须明说「身份与能力照旧」,并且档位取「保留语气风格、不指向第三方、
|
||
// 不外说私事」——不是换人格,也不是中性机械口吻。
|
||
func TestPublicPresenceBlockIsPresentationalProjection(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
// 定位与档位:身份/能力不变、只调呈现、语气风格保留。
|
||
for _, want := range []string{"身份", "能力", "呈现", "语气"} {
|
||
if !strings.Contains(publicPresencePrompt, want) {
|
||
t.Fatalf("公域呈现块 = %q, want it to state the projection framing (%q)", publicPresencePrompt, want)
|
||
}
|
||
}
|
||
// 三条约束:称呼、亲近动作、私事(住址/姓名/作息/行程/健康/家人/关系)。
|
||
for _, want := range []string{
|
||
"亲爱的", "昵称",
|
||
"凑近", "靠过来",
|
||
"住址", "姓名", "作息", "行程", "健康", "家人", "关系",
|
||
"记忆",
|
||
} {
|
||
if !strings.Contains(publicPresencePrompt, want) {
|
||
t.Fatalf("公域呈现块 = %q, want it to cover %q", publicPresencePrompt, want)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestThirdPartyBoundaryCarriesAddressingAndPrivacyRules 钉住 P0-4:
|
||
// 第三方边界提示词除了「谁是谁」,还必须带两条硬规则 —— 不用主人专属的
|
||
// 亲昵称呼称呼第三方、不向第三方外说主人的私事(即使记忆里读到)。
|
||
func TestThirdPartyBoundaryCarriesAddressingAndPrivacyRules(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
for _, want := range []string{"亲昵称呼", "亲爱的", "住址", "作息", "行程", "健康", "家人", "关系", "记忆"} {
|
||
if !strings.Contains(thirdPartyBoundaryPrompt, want) {
|
||
t.Fatalf("第三方边界 = %q, want the P0-4 hard rule keyword %q", thirdPartyBoundaryPrompt, want)
|
||
}
|
||
}
|
||
// 边界仍然先讲归属(裁决 06.4.1),P0-4 只追加、不替换。
|
||
if !strings.Contains(thirdPartyBoundaryPrompt, "发言归属") {
|
||
t.Fatalf("第三方边界 = %q, want the structured-attribution rule to survive", thirdPartyBoundaryPrompt)
|
||
}
|
||
}
|