@sema-agent/client-core 0.52.0 → 0.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +142 -1
- package/README.md +2 -1
- package/dist/adapt/arms.js +31 -3
- package/dist/adapter/activeRunSelfHeal.d.ts +99 -2
- package/dist/adapter/activeRunSelfHeal.js +381 -104
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7 -0
- package/dist/peerFrames.d.ts +117 -0
- package/dist/peerFrames.js +369 -0
- package/docs/INTEGRATION-CLIENTS.md +257 -9
- package/package.json +1 -1
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* peerFrames.ts — design/385 的三条**引擎注入帧**在客户端的类型化投影(判定单源在包,端只装配)。
|
|
3
|
+
*
|
|
4
|
+
* ── 这一件守的是什么 ────────────────────────────────────────────────────────────────
|
|
5
|
+
* 引擎把三类东西塞进 **同一条** `task_notification` 车道,而它们在模型面根本不是同一种东西
|
|
6
|
+
* (core `renderTaskNotificationXml` 对这三类**不套** `<task-notification>` 壳):
|
|
7
|
+
* · `agentMessage` 同进程子代 → 父会话的 uplink(`SendMessage("main")`,§1.4 d1);
|
|
8
|
+
* · `crossSessionMessage` 另一个会话的消息,从本会话自己的信箱里 drain 出来(§4.1);
|
|
9
|
+
* · `crossSessionNotice` 关于**本会话自己发出去的**那条消息的回执 / idle 通知(§4.4 / §5.2)。
|
|
10
|
+
* 端如果照 `task_notification` 泛化卡去渲,用户看到的是一张「后台任务完成」卡,而模型读到的是
|
|
11
|
+
* 一条同事发来的话 —— 两个面说的不是同一件事。
|
|
12
|
+
*
|
|
13
|
+
* 🔴 **判别位是类型化载体的在场,永远不是 `summary` 文本**。这三条载体只有引擎的注入腿铸得出
|
|
14
|
+
* (`ExternalNotificationInput` 是 `TaskNotificationPayload` 的**真子集**,外部 `notify()` 一个
|
|
15
|
+
* 都穿不上);而 `summary`/`result` 是任何一条通知都填的字段,拿它做判据 = 任何一个后台任务
|
|
16
|
+
* 只要把 `<agent-message from="…">` 写进 summary 就能冒充一条同事消息。
|
|
17
|
+
*
|
|
18
|
+
* 🔴 **身份三条等式**:`_sema_provenance` 必须在场、`kind` 与车道相符,且 `from`/`taskId`/`seq`
|
|
19
|
+
* 与载体和载荷逐一相等(三条都由 core 铸点直证,见 `provenanceAgrees`)。只核 `kind` 拦得住
|
|
20
|
+
* 半截载荷,拦不住**同 kind 的伪造**——载体署一个可信名字、provenance 三位全不对,屏上照样
|
|
21
|
+
* 出现一条署着那个名字的消息。
|
|
22
|
+
*
|
|
23
|
+
* 🔴 **fail-closed**:载体在场但形不合(`from` 空、`body` 非串、notice 的 `kind` 不在闭集里)⇒
|
|
24
|
+
* 返回 `null` = 退回泛化卡。退回泛化卡是**诚实降级**(用户仍看得见这条通知,只是没有专用形);
|
|
25
|
+
* 而放一个半截形出去,端就会拿 `undefined` 去渲一张署名为空的「来自 @undefined 的消息」卡。
|
|
26
|
+
*
|
|
27
|
+
* 🔴 **优先序照抄 core 的渲染腿**(`agentMessage` → `crossSessionMessage` → `crossSessionNotice`)。
|
|
28
|
+
* 多载体同时在场是矛盾载荷,但**模型那一面已经按 core 的顺序读过了** —— 端按同一个顺序判,
|
|
29
|
+
* 两个面才说同一件事;这里另立一套「矛盾就退泛化」会让屏上那张卡与模型读到的帧对不上。
|
|
30
|
+
*
|
|
31
|
+
* ── 为什么渲染/解析两半都在包里 ─────────────────────────────────────────────────────
|
|
32
|
+
* 端的消息面(CC 血统:`UserTextMessage` 按标签分派)拿到的只有**文本**,没有帧。所以包必须同时
|
|
33
|
+
* 给出「帧 → 文本」与「文本 → 帧」两半,且**同源**(同一组标签常量、同一套属性序),端才可能
|
|
34
|
+
* 做到零字符串判定。两半的往返在常驻门里逐形对拍。
|
|
35
|
+
*
|
|
36
|
+
* ── 与上游的锚 ──────────────────────────────────────────────────────────────────────
|
|
37
|
+
* 两个标签字面量是 core 的铸点镜像(`core/task-notification.js::AGENT_MESSAGE_TAG` /
|
|
38
|
+
* `agents/cross-session-envelope.js::CROSS_SESSION_MESSAGE_TAG`),登记在壳的 wire 锚契约表里
|
|
39
|
+
* (cli `scripts/wire-anchor-registry.mjs`)—— core 改词当天那道门就红,而不是等到用户发现
|
|
40
|
+
* 「同事消息又变回一张后台任务卡了」。
|
|
41
|
+
*/
|
|
42
|
+
/** core `core/task-notification.ts::AGENT_MESSAGE_TAG` 的镜像(同进程 uplink 的模型面标签)。 */
|
|
43
|
+
export declare const AGENT_MESSAGE_TAG = "agent-message";
|
|
44
|
+
/** core `agents/cross-session-envelope.ts::CROSS_SESSION_MESSAGE_TAG` 的镜像(跨会话信封标签)。 */
|
|
45
|
+
export declare const CROSS_SESSION_MESSAGE_TAG = "cross-session-message";
|
|
46
|
+
/** core `_sema_provenance.kind` 的闭集(7.4.0 三员;7.2.0 只有第一员 —— 缺员按「上游还没发」处理,
|
|
47
|
+
* 不是「上游改词」:本模块从不要求 provenance 在场,只要求**在场时不自相矛盾**)。 */
|
|
48
|
+
export declare const PEER_FRAME_LANES: readonly ["agent_message", "cross_session_message", "cross_session_notice"];
|
|
49
|
+
export type PeerFrameLane = (typeof PEER_FRAME_LANES)[number];
|
|
50
|
+
/** core `crossSessionNotice.kind` 的闭集(§4.4:投递回执 / idle 回执两形)。 */
|
|
51
|
+
export declare const CROSS_SESSION_NOTICE_KINDS: readonly ["delivery_notice", "idle_notice"];
|
|
52
|
+
export type CrossSessionNoticeKind = (typeof CROSS_SESSION_NOTICE_KINDS)[number];
|
|
53
|
+
/** core `PermissionModeClass`(发送方**自述**的权限模式类,不是被证实的事实)。 */
|
|
54
|
+
export declare const PEER_MODE_CLASSES: readonly ["bypass", "prompting"];
|
|
55
|
+
export type PeerModeClass = (typeof PEER_MODE_CLASSES)[number];
|
|
56
|
+
export type AgentMessageFrame = {
|
|
57
|
+
lane: 'agent_message';
|
|
58
|
+
/** 帧的 `from="…"` 属性所拼的那个署名(子代的名字 / 标签)。 */
|
|
59
|
+
from: string;
|
|
60
|
+
body: string;
|
|
61
|
+
agentType?: string;
|
|
62
|
+
/** 生产者的 per-frame 计数器(= 本 payload 的 `seq`);缺席不补 0。 */
|
|
63
|
+
seq?: number;
|
|
64
|
+
};
|
|
65
|
+
export type CrossSessionMessageFrame = {
|
|
66
|
+
lane: 'cross_session_message';
|
|
67
|
+
/** 发送方的**地址**(可回信的那一个;§4.1 provenance 的 `from` 同值)。 */
|
|
68
|
+
from: string;
|
|
69
|
+
body: string;
|
|
70
|
+
fromSession?: string;
|
|
71
|
+
fromName?: string;
|
|
72
|
+
fromMode?: PeerModeClass;
|
|
73
|
+
fromScope?: string;
|
|
74
|
+
seq?: number;
|
|
75
|
+
};
|
|
76
|
+
export type CrossSessionNoticeFrame = {
|
|
77
|
+
lane: 'cross_session_notice';
|
|
78
|
+
kind: CrossSessionNoticeKind;
|
|
79
|
+
/** 引擎自铸的整行文本(`[Cross-session delivery notice] …` / `[Cross-session idle notice] …`)。
|
|
80
|
+
* 🔴 端**原样渲一行**:这行是引擎按类型化 `peerMeta.notice` 铸的,端再解析一遍就是拿散文当数据。 */
|
|
81
|
+
text: string;
|
|
82
|
+
};
|
|
83
|
+
export type PeerFrameProjection = AgentMessageFrame | CrossSessionMessageFrame | CrossSessionNoticeFrame;
|
|
84
|
+
/**
|
|
85
|
+
* `task_notification` 原始 wire 载荷 → 三条引擎注入车道之一;认不出 ⇒ `null`(= 泛化通知,
|
|
86
|
+
* 调用方照旧走 `normalizeTaskNotification` / `renderTaskNotificationXml`)。
|
|
87
|
+
*
|
|
88
|
+
* 入参是**原始**载荷(snake_case `task_id` 那一份),不是 `normalizeTaskNotification` 的产物 ——
|
|
89
|
+
* 后者是一张 15 键白名单,这三条载体一个都不在里面。
|
|
90
|
+
*/
|
|
91
|
+
export declare function classifyPeerNotification(n: Record<string, unknown>): PeerFrameProjection | null;
|
|
92
|
+
/**
|
|
93
|
+
* core `ENGINE_AUTHORITY_ENVELOPE_TAGS` 的镜像(`core/untrusted-text.ts::ENGINE_ENVELOPES` 里 kind="authority" 的标签):
|
|
94
|
+
* 这些信封在模型面/转录面代表**引擎权威**,一段同事正文里出现它们就是伪造。core 的 `neutralizePeerBody`
|
|
95
|
+
* (= `sanitizeUntrustedText(body, PEER_BODY_ENVELOPE_TAGS)`)在渲染信封**之前**先把它们拆火(`<` 后插 ZWSP),
|
|
96
|
+
* 本模块首版只抄了后一步(同名信封拆火)——异源发包扫描 [high] 实证:子代正文里一段
|
|
97
|
+
* `<task-notification><task-id>victim</task-id><status>completed</status>…` 会原样进转录 block,
|
|
98
|
+
* 宿主 resume 时 `parseTranscriptNotificationSeeds` 把它读成真完成通知 ⇒ 去重台账被毒化,受害 run 的
|
|
99
|
+
* 真完成通知随后被跨通道臂整条吞掉。这里补上那一步,并与 core 同形(`<\s*\/?\s*(tag)(\s[^>]*)?>` 全大小写)。
|
|
100
|
+
* 🔴 单向(与 core 一致):端的解析腿**不**还原 ZWSP —— 拆掉的权威标签就该永远是拆掉的。
|
|
101
|
+
*/
|
|
102
|
+
export declare const AUTHORITY_ENVELOPE_TAGS: readonly ["system-reminder", "task-notification", "new-diagnostics", "user_memory", "scope", "skills", "total_tokens", "instruction-files"];
|
|
103
|
+
/**
|
|
104
|
+
* 投影 → 转录行文本(端的消息面按标签分派的那一份)。
|
|
105
|
+
*
|
|
106
|
+
* 🔴 这**不是** core 模型面信封的逐字副本,也不该是:core 那一份是给**模型**读的(带纪律块、
|
|
107
|
+
* 带 CC 逐字属性语法、带 round-trip 拒收);这一份是**端的转录回显**,只需要与本模块的解析腿
|
|
108
|
+
* 往返一致。刻意不复刻 core 的语法契约 —— 复刻一份对不上的副本比不复刻更坏
|
|
109
|
+
* (它会让人以为端这一行是模型读到的那一行)。属性序仍照 core 的规范序排,便于人肉比对。
|
|
110
|
+
*/
|
|
111
|
+
export declare function renderPeerFrameTranscriptText(p: PeerFrameProjection): string;
|
|
112
|
+
export declare function parsePeerFrameText(text: string): AgentMessageFrame | CrossSessionMessageFrame | null;
|
|
113
|
+
/**
|
|
114
|
+
* 端渲「@谁」时该用的那个名字 —— 判定在包,端不许自己排优先序。
|
|
115
|
+
* 跨会话:`fromName`(人取的名)优先于 `from`(地址);同进程:只有 `from`。
|
|
116
|
+
*/
|
|
117
|
+
export declare function peerFrameDisplayName(p: AgentMessageFrame | CrossSessionMessageFrame): string;
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* peerFrames.ts — design/385 的三条**引擎注入帧**在客户端的类型化投影(判定单源在包,端只装配)。
|
|
3
|
+
*
|
|
4
|
+
* ── 这一件守的是什么 ────────────────────────────────────────────────────────────────
|
|
5
|
+
* 引擎把三类东西塞进 **同一条** `task_notification` 车道,而它们在模型面根本不是同一种东西
|
|
6
|
+
* (core `renderTaskNotificationXml` 对这三类**不套** `<task-notification>` 壳):
|
|
7
|
+
* · `agentMessage` 同进程子代 → 父会话的 uplink(`SendMessage("main")`,§1.4 d1);
|
|
8
|
+
* · `crossSessionMessage` 另一个会话的消息,从本会话自己的信箱里 drain 出来(§4.1);
|
|
9
|
+
* · `crossSessionNotice` 关于**本会话自己发出去的**那条消息的回执 / idle 通知(§4.4 / §5.2)。
|
|
10
|
+
* 端如果照 `task_notification` 泛化卡去渲,用户看到的是一张「后台任务完成」卡,而模型读到的是
|
|
11
|
+
* 一条同事发来的话 —— 两个面说的不是同一件事。
|
|
12
|
+
*
|
|
13
|
+
* 🔴 **判别位是类型化载体的在场,永远不是 `summary` 文本**。这三条载体只有引擎的注入腿铸得出
|
|
14
|
+
* (`ExternalNotificationInput` 是 `TaskNotificationPayload` 的**真子集**,外部 `notify()` 一个
|
|
15
|
+
* 都穿不上);而 `summary`/`result` 是任何一条通知都填的字段,拿它做判据 = 任何一个后台任务
|
|
16
|
+
* 只要把 `<agent-message from="…">` 写进 summary 就能冒充一条同事消息。
|
|
17
|
+
*
|
|
18
|
+
* 🔴 **身份三条等式**:`_sema_provenance` 必须在场、`kind` 与车道相符,且 `from`/`taskId`/`seq`
|
|
19
|
+
* 与载体和载荷逐一相等(三条都由 core 铸点直证,见 `provenanceAgrees`)。只核 `kind` 拦得住
|
|
20
|
+
* 半截载荷,拦不住**同 kind 的伪造**——载体署一个可信名字、provenance 三位全不对,屏上照样
|
|
21
|
+
* 出现一条署着那个名字的消息。
|
|
22
|
+
*
|
|
23
|
+
* 🔴 **fail-closed**:载体在场但形不合(`from` 空、`body` 非串、notice 的 `kind` 不在闭集里)⇒
|
|
24
|
+
* 返回 `null` = 退回泛化卡。退回泛化卡是**诚实降级**(用户仍看得见这条通知,只是没有专用形);
|
|
25
|
+
* 而放一个半截形出去,端就会拿 `undefined` 去渲一张署名为空的「来自 @undefined 的消息」卡。
|
|
26
|
+
*
|
|
27
|
+
* 🔴 **优先序照抄 core 的渲染腿**(`agentMessage` → `crossSessionMessage` → `crossSessionNotice`)。
|
|
28
|
+
* 多载体同时在场是矛盾载荷,但**模型那一面已经按 core 的顺序读过了** —— 端按同一个顺序判,
|
|
29
|
+
* 两个面才说同一件事;这里另立一套「矛盾就退泛化」会让屏上那张卡与模型读到的帧对不上。
|
|
30
|
+
*
|
|
31
|
+
* ── 为什么渲染/解析两半都在包里 ─────────────────────────────────────────────────────
|
|
32
|
+
* 端的消息面(CC 血统:`UserTextMessage` 按标签分派)拿到的只有**文本**,没有帧。所以包必须同时
|
|
33
|
+
* 给出「帧 → 文本」与「文本 → 帧」两半,且**同源**(同一组标签常量、同一套属性序),端才可能
|
|
34
|
+
* 做到零字符串判定。两半的往返在常驻门里逐形对拍。
|
|
35
|
+
*
|
|
36
|
+
* ── 与上游的锚 ──────────────────────────────────────────────────────────────────────
|
|
37
|
+
* 两个标签字面量是 core 的铸点镜像(`core/task-notification.js::AGENT_MESSAGE_TAG` /
|
|
38
|
+
* `agents/cross-session-envelope.js::CROSS_SESSION_MESSAGE_TAG`),登记在壳的 wire 锚契约表里
|
|
39
|
+
* (cli `scripts/wire-anchor-registry.mjs`)—— core 改词当天那道门就红,而不是等到用户发现
|
|
40
|
+
* 「同事消息又变回一张后台任务卡了」。
|
|
41
|
+
*/
|
|
42
|
+
// ── wire 键名 / 属性名的单源(**刻意放在本文件第一个 `export` 之前**)────────────────────────
|
|
43
|
+
// 两个作用,第二个是被逼出来的:
|
|
44
|
+
// ① 键名单源 —— 读口与渲染口用同一个字节,不给手抄留缝;
|
|
45
|
+
// ② 绕开 client-core 闭包扫描器的一处误匹配:`run-client-core-portability-test.mjs` 的
|
|
46
|
+
// `import/export … from '…'` 边扫描按 `from` + 引号这个**字节序列**认依赖,于是源码里任何
|
|
47
|
+
// 一个 `'from'` 字面量(本模块读的正是 wire 上那个 `from` 键)都会被当成一条外部包依赖,
|
|
48
|
+
// 把「A 层闭包外部包 === {diff}」判红。扫描器的锚起点是**行首的 `export`**,所以键名收在
|
|
49
|
+
// 首个 export 之前就落在它的射程外。⚠️ 这是绕过一处**扫描器误报**,不是绕过判据本身 ——
|
|
50
|
+
// 本模块真实的外部依赖是零(纯叶,零 import)。
|
|
51
|
+
const WIRE_KEY_FROM = 'from';
|
|
52
|
+
const WIRE_KEY_BODY = 'body';
|
|
53
|
+
const WIRE_KEY_KIND = 'kind';
|
|
54
|
+
const WIRE_KEY_TEXT = 'text';
|
|
55
|
+
const WIRE_KEY_SEQ = 'seq';
|
|
56
|
+
const WIRE_KEY_TASK_ID = 'task_id';
|
|
57
|
+
/** 🔴 载荷那一边是 **snake** `task_id`,provenance 那一边是 **camel** `taskId` —— 同一个量、两个拼法,
|
|
58
|
+
* 写混了等式恒不成立、整条车道静默死掉(本模块首版就写混过一次,当场被门抓住)。 */
|
|
59
|
+
const WIRE_KEY_PROV_TASK_ID = 'taskId';
|
|
60
|
+
const WIRE_KEY_AGENT_TYPE = 'agentType';
|
|
61
|
+
const WIRE_KEY_AGENT_MESSAGE = 'agentMessage';
|
|
62
|
+
const WIRE_KEY_CROSS_SESSION_MESSAGE = 'crossSessionMessage';
|
|
63
|
+
const WIRE_KEY_CROSS_SESSION_NOTICE = 'crossSessionNotice';
|
|
64
|
+
const WIRE_KEY_PROVENANCE = '_sema_provenance';
|
|
65
|
+
const WIRE_KEY_FROM_SESSION = 'fromSession';
|
|
66
|
+
const WIRE_KEY_FROM_NAME = 'fromName';
|
|
67
|
+
const WIRE_KEY_FROM_MODE = 'fromMode';
|
|
68
|
+
const WIRE_KEY_FROM_SCOPE = 'fromScope';
|
|
69
|
+
/** 信封上的 `from` 属性名(与上面的 wire 键同字节,渲染/解析两腿共用)。 */
|
|
70
|
+
const FROM_ATTR = WIRE_KEY_FROM;
|
|
71
|
+
/** core `core/task-notification.ts::AGENT_MESSAGE_TAG` 的镜像(同进程 uplink 的模型面标签)。 */
|
|
72
|
+
export const AGENT_MESSAGE_TAG = 'agent-message';
|
|
73
|
+
/** core `agents/cross-session-envelope.ts::CROSS_SESSION_MESSAGE_TAG` 的镜像(跨会话信封标签)。 */
|
|
74
|
+
export const CROSS_SESSION_MESSAGE_TAG = 'cross-session-message';
|
|
75
|
+
/** core `_sema_provenance.kind` 的闭集(7.4.0 三员;7.2.0 只有第一员 —— 缺员按「上游还没发」处理,
|
|
76
|
+
* 不是「上游改词」:本模块从不要求 provenance 在场,只要求**在场时不自相矛盾**)。 */
|
|
77
|
+
export const PEER_FRAME_LANES = Object.freeze(['agent_message', 'cross_session_message', 'cross_session_notice']);
|
|
78
|
+
/** core `crossSessionNotice.kind` 的闭集(§4.4:投递回执 / idle 回执两形)。 */
|
|
79
|
+
export const CROSS_SESSION_NOTICE_KINDS = Object.freeze(['delivery_notice', 'idle_notice']);
|
|
80
|
+
/** core `PermissionModeClass`(发送方**自述**的权限模式类,不是被证实的事实)。 */
|
|
81
|
+
export const PEER_MODE_CLASSES = Object.freeze(['bypass', 'prompting']);
|
|
82
|
+
// ── 防御式读口 ────────────────────────────────────────────────────────────────────────
|
|
83
|
+
// 载荷是 wire JSON,正常路径上全是数据属性;但读一个 getter = 同步跑别人的代码,而
|
|
84
|
+
// `try/catch` 接得住抛错、接不住「永不返回」。⇒ 只认自有**数据**描述符,访问器一律当缺席。
|
|
85
|
+
function dataProp(o, key) {
|
|
86
|
+
if (o === null || typeof o !== 'object')
|
|
87
|
+
return undefined;
|
|
88
|
+
let d;
|
|
89
|
+
try {
|
|
90
|
+
d = Object.getOwnPropertyDescriptor(o, key);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// 已撤销的 Proxy / 敌意 trap:当缺席(读不出来 ≠ 有值)
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
if (d === undefined || !('value' in d))
|
|
97
|
+
return undefined;
|
|
98
|
+
return d.value;
|
|
99
|
+
}
|
|
100
|
+
function nonEmptyString(v) {
|
|
101
|
+
return typeof v === 'string' && v.length > 0 ? v : undefined;
|
|
102
|
+
}
|
|
103
|
+
function finiteNumber(v) {
|
|
104
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : undefined;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* `_sema_provenance` 的一致性判据 —— **必须在场、且 `kind` 与载体所属车道相符**。
|
|
108
|
+
*
|
|
109
|
+
* 🔴 首版写的是「只否决不认证、缺席放行」,理由是「core 7.2.0 的闭集只有一员,要求在场会让另外
|
|
110
|
+
* 两条车道在老引擎上恒死」。**那条理由是错的**(异源对抗复审 r1 [high] 证伪,当场采纳):
|
|
111
|
+
* 老引擎上另外两条车道**连载体键都不存在**,本函数根本走不到;而 core 契约明写 provenance
|
|
112
|
+
* 「present exactly when the carrier is」——今天在跑的那一版(7.2.0 `agents/send-message-tool.js`
|
|
113
|
+
* 的 uplink 腿)就是**同一处**同时铸载体与 provenance 的。所以要求它在场:
|
|
114
|
+
* · 对**真**载荷零代价(真载荷从来都带着它);
|
|
115
|
+
* · 对畸形/半截注入/版本漂移的载荷则关上了一道门 —— 否则只凭 `from`/`body` 两个字符串就能
|
|
116
|
+
* 让屏上出现一张署着任意名字的「来自某人的消息」卡,而那正是本模块存在的理由的反面。
|
|
117
|
+
*
|
|
118
|
+
* 判据 = **在场 + 四等式**(`kind` 与车道相符;`from`/`taskId`/`seq` 三位必须在场且与载体、载荷逐一相等,
|
|
119
|
+
* 见函数体内三条铸点直证)。首版曾写「判据只到 kind、刻意不比三位」——那一版的顾虑(上游编码差一格
|
|
120
|
+
* ⇒ 车道静默死)由 cli 侧 wire 锚 A-K24 的同址探针接管:铸点一改门先红,不会变成静默死。
|
|
121
|
+
*/
|
|
122
|
+
function provenanceAgrees(n, lane, carrierFrom) {
|
|
123
|
+
const prov = dataProp(n, WIRE_KEY_PROVENANCE);
|
|
124
|
+
if (prov === null || typeof prov !== 'object')
|
|
125
|
+
return false;
|
|
126
|
+
if (dataProp(prov, WIRE_KEY_KIND) !== lane)
|
|
127
|
+
return false;
|
|
128
|
+
// 🔴 三条等式**每一条都有铸点直证**(不是从散文里推的):
|
|
129
|
+
// · uplink 腿(core 7.2.0 `dist/agents/send-message-tool.js`)一个对象字面量里同时写下
|
|
130
|
+
// `task_id: senderId` / `seq: uplinkSeq` / `agentMessage:{from: senderLabel}` 与
|
|
131
|
+
// `_sema_provenance:{from: senderLabel, taskId: senderId, seq: uplinkSeq}`;
|
|
132
|
+
// · 跨会话 drain 腿(core 7.4.0 `agents/peer-session-drain.ts` 的两个 build*Payload)同形:
|
|
133
|
+
// `task_id: boxHandle` / `seq: m.seq` 与 provenance 的 `taskId`/`seq` 同值,
|
|
134
|
+
// 消息臂的 `crossSessionMessage.from` 与 provenance 的 `from` 同为 `fields.from`。
|
|
135
|
+
// ⇒ 三位不一致 = 这不是引擎那条腿铸出来的载荷。只核 `kind` 只拦得住半截载荷,拦不住
|
|
136
|
+
// **同 kind 的伪造**:载体署一个可信的名字、provenance 三位全对不上,屏上照样出现一条
|
|
137
|
+
// 署着那个名字的消息(异源对抗复审 r2 [medium] 实测)。
|
|
138
|
+
// 🔴 铸点一改这三条就该跟着改 —— 由壳的 wire 锚 A-K24 同址探针钉住那一行字面量,
|
|
139
|
+
// 上游改词当天门就红,不会变成一条静默死掉的车道。
|
|
140
|
+
// 🔴 **在场也是判据的一半**(异源对抗复审 r3 [medium] 采纳):`SemaProvenance` 的 `from`/`taskId`/
|
|
141
|
+
// `seq` 在 core 的类型面上**全是必填**,三个铸点也都同址写下它们 —— 所以「两侧都缺」不是一种
|
|
142
|
+
// 合法的兼容形,而是一个半截载荷。首版只写 `a === b`,于是 `undefined === undefined` 把它放行了
|
|
143
|
+
// (实测:去掉两侧 seq、或去掉载荷的 task_id,半截载荷照样投影成一条 agent_message)。
|
|
144
|
+
// ⇒ 先各自核在场与类型,再比值。
|
|
145
|
+
const provFrom = cleanName(dataProp(prov, WIRE_KEY_FROM));
|
|
146
|
+
if (provFrom === undefined || (carrierFrom !== undefined && provFrom !== carrierFrom))
|
|
147
|
+
return false;
|
|
148
|
+
const taskId = nonEmptyString(dataProp(n, WIRE_KEY_TASK_ID));
|
|
149
|
+
if (taskId === undefined || nonEmptyString(dataProp(prov, WIRE_KEY_PROV_TASK_ID)) !== taskId)
|
|
150
|
+
return false;
|
|
151
|
+
const seq = finiteNumber(dataProp(n, WIRE_KEY_SEQ));
|
|
152
|
+
return seq !== undefined && finiteNumber(dataProp(prov, WIRE_KEY_SEQ)) === seq;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* `task_notification` 原始 wire 载荷 → 三条引擎注入车道之一;认不出 ⇒ `null`(= 泛化通知,
|
|
156
|
+
* 调用方照旧走 `normalizeTaskNotification` / `renderTaskNotificationXml`)。
|
|
157
|
+
*
|
|
158
|
+
* 入参是**原始**载荷(snake_case `task_id` 那一份),不是 `normalizeTaskNotification` 的产物 ——
|
|
159
|
+
* 后者是一张 15 键白名单,这三条载体一个都不在里面。
|
|
160
|
+
*/
|
|
161
|
+
export function classifyPeerNotification(n) {
|
|
162
|
+
const seq = finiteNumber(dataProp(n, WIRE_KEY_SEQ));
|
|
163
|
+
// ① 同进程 uplink(§1.4 d1)
|
|
164
|
+
const agentMessage = dataProp(n, WIRE_KEY_AGENT_MESSAGE);
|
|
165
|
+
if (agentMessage !== undefined) {
|
|
166
|
+
const from = cleanName(dataProp(agentMessage, WIRE_KEY_FROM));
|
|
167
|
+
const body = dataProp(agentMessage, WIRE_KEY_BODY);
|
|
168
|
+
if (from !== undefined && typeof body === 'string' && provenanceAgrees(n, 'agent_message', from)) {
|
|
169
|
+
const agentType = nonEmptyString(dataProp(dataProp(n, WIRE_KEY_PROVENANCE), WIRE_KEY_AGENT_TYPE));
|
|
170
|
+
return {
|
|
171
|
+
lane: 'agent_message',
|
|
172
|
+
from,
|
|
173
|
+
body,
|
|
174
|
+
...(agentType !== undefined ? { agentType } : {}),
|
|
175
|
+
...(seq !== undefined ? { seq } : {}),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
// ② 跨会话信封(§4.1)
|
|
181
|
+
const crossSessionMessage = dataProp(n, WIRE_KEY_CROSS_SESSION_MESSAGE);
|
|
182
|
+
if (crossSessionMessage !== undefined) {
|
|
183
|
+
const from = cleanName(dataProp(crossSessionMessage, WIRE_KEY_FROM));
|
|
184
|
+
const body = dataProp(crossSessionMessage, WIRE_KEY_BODY);
|
|
185
|
+
if (from === undefined || typeof body !== 'string' || !provenanceAgrees(n, 'cross_session_message', from))
|
|
186
|
+
return null;
|
|
187
|
+
const fromSession = nonEmptyString(dataProp(crossSessionMessage, WIRE_KEY_FROM_SESSION));
|
|
188
|
+
const fromName = cleanName(dataProp(crossSessionMessage, WIRE_KEY_FROM_NAME));
|
|
189
|
+
const fromModeRaw = dataProp(crossSessionMessage, WIRE_KEY_FROM_MODE);
|
|
190
|
+
const fromMode = PEER_MODE_CLASSES.find(m => m === fromModeRaw);
|
|
191
|
+
const fromScope = nonEmptyString(dataProp(crossSessionMessage, WIRE_KEY_FROM_SCOPE));
|
|
192
|
+
// 🔴 可选位形不合 ⇒ **只丢那一位**,不丢整帧:少一个 `from-mode` 是少一句注,
|
|
193
|
+
// 丢整帧是把一条同事消息从用户眼前拿走(与必填位的 fail-closed 是两码事)。
|
|
194
|
+
return {
|
|
195
|
+
lane: 'cross_session_message',
|
|
196
|
+
from,
|
|
197
|
+
body,
|
|
198
|
+
...(fromSession !== undefined ? { fromSession } : {}),
|
|
199
|
+
...(fromName !== undefined ? { fromName } : {}),
|
|
200
|
+
...(fromMode !== undefined ? { fromMode } : {}),
|
|
201
|
+
...(fromScope !== undefined ? { fromScope } : {}),
|
|
202
|
+
...(seq !== undefined ? { seq } : {}),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
// ③ 跨会话回执 / idle 通知(§4.4 / §5.2)
|
|
206
|
+
const crossSessionNotice = dataProp(n, WIRE_KEY_CROSS_SESSION_NOTICE);
|
|
207
|
+
if (crossSessionNotice !== undefined) {
|
|
208
|
+
const kindRaw = dataProp(crossSessionNotice, WIRE_KEY_KIND);
|
|
209
|
+
const kind = CROSS_SESSION_NOTICE_KINDS.find(k => k === kindRaw);
|
|
210
|
+
const text = nonEmptyString(dataProp(crossSessionNotice, WIRE_KEY_TEXT));
|
|
211
|
+
if (kind === undefined || text === undefined || !provenanceAgrees(n, 'cross_session_notice'))
|
|
212
|
+
return null;
|
|
213
|
+
return { lane: 'cross_session_notice', kind, text };
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
// ── 帧 ⇄ 文本(端的消息面只拿得到文本;两半同源,往返在常驻门里对拍)────────────────────
|
|
218
|
+
/**
|
|
219
|
+
* core `ENGINE_AUTHORITY_ENVELOPE_TAGS` 的镜像(`core/untrusted-text.ts::ENGINE_ENVELOPES` 里 kind="authority" 的标签):
|
|
220
|
+
* 这些信封在模型面/转录面代表**引擎权威**,一段同事正文里出现它们就是伪造。core 的 `neutralizePeerBody`
|
|
221
|
+
* (= `sanitizeUntrustedText(body, PEER_BODY_ENVELOPE_TAGS)`)在渲染信封**之前**先把它们拆火(`<` 后插 ZWSP),
|
|
222
|
+
* 本模块首版只抄了后一步(同名信封拆火)——异源发包扫描 [high] 实证:子代正文里一段
|
|
223
|
+
* `<task-notification><task-id>victim</task-id><status>completed</status>…` 会原样进转录 block,
|
|
224
|
+
* 宿主 resume 时 `parseTranscriptNotificationSeeds` 把它读成真完成通知 ⇒ 去重台账被毒化,受害 run 的
|
|
225
|
+
* 真完成通知随后被跨通道臂整条吞掉。这里补上那一步,并与 core 同形(`<\s*\/?\s*(tag)(\s[^>]*)?>` 全大小写)。
|
|
226
|
+
* 🔴 单向(与 core 一致):端的解析腿**不**还原 ZWSP —— 拆掉的权威标签就该永远是拆掉的。
|
|
227
|
+
*/
|
|
228
|
+
export const AUTHORITY_ENVELOPE_TAGS = Object.freeze([
|
|
229
|
+
'system-reminder',
|
|
230
|
+
'task-notification',
|
|
231
|
+
'new-diagnostics',
|
|
232
|
+
'user_memory',
|
|
233
|
+
'scope',
|
|
234
|
+
'skills',
|
|
235
|
+
'total_tokens',
|
|
236
|
+
'instruction-files',
|
|
237
|
+
]);
|
|
238
|
+
const ZWSP = String.fromCharCode(0x200b);
|
|
239
|
+
const AUTHORITY_BREAKOUT_RE = new RegExp(`<\\s*\\/?\\s*(?:${AUTHORITY_ENVELOPE_TAGS.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})(?:\\s[^>]*)?>`, 'gi');
|
|
240
|
+
function neutralizeAuthorityTags(text) {
|
|
241
|
+
return text.replace(AUTHORITY_BREAKOUT_RE, m => m.replace('<', '<' + ZWSP));
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* 署名类属性的呈现规范化(异源发包扫描 [medium]):`from` / `fromName` 是发送方**自述**的串,可含换行与控制符,
|
|
245
|
+
* 折叠行按单行渲会被它撑破。与 core `canonicalPeerDisplayName` 同向:控制符/换行折成单空格、两端去空白;
|
|
246
|
+
* 折空 ⇒ 视同缺席(必填位 ⇒ 整帧退泛化卡;可选位 ⇒ 只丢那一位)。
|
|
247
|
+
*/
|
|
248
|
+
function cleanName(v) {
|
|
249
|
+
if (typeof v !== 'string')
|
|
250
|
+
return undefined;
|
|
251
|
+
const t = v.replace(/[\u0000-\u001f\u007f\u2028\u2029]+/g, ' ').replace(/\s+/g, ' ').trim();
|
|
252
|
+
return t.length > 0 ? t : undefined;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* 正文里的同名标签拆火:开括号后插一个反斜杠,于是它不再是一个标签。
|
|
256
|
+
*
|
|
257
|
+
* 🔴 与 core 的 `escapeEnvelopeTag` **有意不同一格**(异源对抗复审 r1 [medium] 采纳):core 那一份是
|
|
258
|
+
* **单向**消毒(模型读到就完了,没有反向腿),所以它不处理正文里**原本就有**的 `<\agent-message>`;
|
|
259
|
+
* 而本模块有反向腿(端的消息面要把正文还原出来),单向消毒在这里就是一个**非单射**映射 ——
|
|
260
|
+
* 一段合法的代码文本 `<\agent-message>` 会在 render→parse 往返之后被改写成 `<agent-message>`,
|
|
261
|
+
* 既损坏正文,又等于给正文一条**注入真标签**的路。
|
|
262
|
+
* ⇒ 这里连**已有的反斜杠**一起数:`<` 后面跟任意多个反斜杠再跟标签名时都插一个。
|
|
263
|
+
* 每次编码恰好加一个、每次解码恰好去一个 ⇒ `unescape(escape(x)) === x` 对一切 x 成立。
|
|
264
|
+
*/
|
|
265
|
+
function escapeEnvelopeTag(tag, text) {
|
|
266
|
+
return text.replace(new RegExp(`<(?=\\\\*/?${tag}(?:[>\\s/]|$))`, 'gi'), '<\\');
|
|
267
|
+
}
|
|
268
|
+
function unescapeEnvelopeTag(tag, text) {
|
|
269
|
+
return text.replace(new RegExp(`<\\\\(?=\\\\*/?${tag}(?:[>\\s/]|$))`, 'gi'), '<');
|
|
270
|
+
}
|
|
271
|
+
/** 属性值转义 —— 属性用双引号包,值里的 `"` 与尖括号一律实体化,不给伪造闭合属性留缝。 */
|
|
272
|
+
function attr(v) {
|
|
273
|
+
return v.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
274
|
+
}
|
|
275
|
+
function unattr(v) {
|
|
276
|
+
return v.replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&');
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* 投影 → 转录行文本(端的消息面按标签分派的那一份)。
|
|
280
|
+
*
|
|
281
|
+
* 🔴 这**不是** core 模型面信封的逐字副本,也不该是:core 那一份是给**模型**读的(带纪律块、
|
|
282
|
+
* 带 CC 逐字属性语法、带 round-trip 拒收);这一份是**端的转录回显**,只需要与本模块的解析腿
|
|
283
|
+
* 往返一致。刻意不复刻 core 的语法契约 —— 复刻一份对不上的副本比不复刻更坏
|
|
284
|
+
* (它会让人以为端这一行是模型读到的那一行)。属性序仍照 core 的规范序排,便于人肉比对。
|
|
285
|
+
*/
|
|
286
|
+
export function renderPeerFrameTranscriptText(p) {
|
|
287
|
+
// 🔴 通知纯行也是引擎按 peerMeta 铸的散文,但它承载的仍是**关于别人消息**的文本:同样过权威标签拆火,
|
|
288
|
+
// 并把两个消息信封标签拆火 —— 否则一条整体形如 `<agent-message …>…</agent-message>` 的通知会被端判成
|
|
289
|
+
// 一条署名消息(异源发包扫描同形存量③)。
|
|
290
|
+
if (p.lane === 'cross_session_notice')
|
|
291
|
+
return escapeEnvelopeTag(CROSS_SESSION_MESSAGE_TAG, escapeEnvelopeTag(AGENT_MESSAGE_TAG, neutralizeAuthorityTags(p.text)));
|
|
292
|
+
if (p.lane === 'agent_message') {
|
|
293
|
+
const body = escapeEnvelopeTag(AGENT_MESSAGE_TAG, neutralizeAuthorityTags(p.body));
|
|
294
|
+
return `<${AGENT_MESSAGE_TAG} ${FROM_ATTR}="${attr(p.from)}">\n${body}\n</${AGENT_MESSAGE_TAG}>`;
|
|
295
|
+
}
|
|
296
|
+
const attrs = [`${FROM_ATTR}="${attr(p.from)}"`];
|
|
297
|
+
if (p.fromSession !== undefined)
|
|
298
|
+
attrs.push(`from-session="${attr(p.fromSession)}"`);
|
|
299
|
+
if (p.fromName !== undefined)
|
|
300
|
+
attrs.push(`from-name="${attr(p.fromName)}"`);
|
|
301
|
+
if (p.fromMode !== undefined)
|
|
302
|
+
attrs.push(`from-mode="${attr(p.fromMode)}"`);
|
|
303
|
+
if (p.fromScope !== undefined)
|
|
304
|
+
attrs.push(`from-scope="${attr(p.fromScope)}"`);
|
|
305
|
+
const body = escapeEnvelopeTag(CROSS_SESSION_MESSAGE_TAG, neutralizeAuthorityTags(p.body));
|
|
306
|
+
return `<${CROSS_SESSION_MESSAGE_TAG} ${attrs.join(' ')}>\n${body}\n</${CROSS_SESSION_MESSAGE_TAG}>`;
|
|
307
|
+
}
|
|
308
|
+
// 🔴 属性值不设长度上界:渲染腿无界,解析腿封顶就会让包自己铸出的帧解析回 null(异源发包扫描 [high]);
|
|
309
|
+
// `[^"]*` 是线性匹配,无回溯风险。
|
|
310
|
+
const AGENT_MESSAGE_RE = new RegExp(`^<${AGENT_MESSAGE_TAG} ${FROM_ATTR}="([^"]*)">\\n([\\s\\S]*)\\n</${AGENT_MESSAGE_TAG}>$`);
|
|
311
|
+
const CROSS_SESSION_RE = new RegExp(`^<${CROSS_SESSION_MESSAGE_TAG} ${FROM_ATTR}="([^"]*)"` +
|
|
312
|
+
`(?: from-session="([^"]*)")?` +
|
|
313
|
+
`(?: from-name="([^"]*)")?` +
|
|
314
|
+
`(?: from-mode="(bypass|prompting)")?` +
|
|
315
|
+
`(?: from-scope="([^"]*)")?` +
|
|
316
|
+
`>\\n([\\s\\S]*)\\n</${CROSS_SESSION_MESSAGE_TAG}>$`);
|
|
317
|
+
/**
|
|
318
|
+
* 转录行文本 → 投影(`renderPeerFrameTranscriptText` 的逆)。认不出 ⇒ `null`。
|
|
319
|
+
*
|
|
320
|
+
* 端唯一该调的字符串判定口:**端自己一条正则都不许写**(cli-191 A-D1/A-D12 那一族的成因就是
|
|
321
|
+
* 消费端各自抄一份判据,上游改词后集体空转而没有一道门响)。
|
|
322
|
+
*
|
|
323
|
+
* 🔴 `ok` 只等于「形是规范的」,**不等于**「这条消息真是引擎注入的」——文本面的身份权威不存在
|
|
324
|
+
* (core §4.2 三层规则同一句话)。端拿它做**呈现**分派,不许拿它做任何授权判断。
|
|
325
|
+
* 🔴 通知车道(`cross_session_notice`)刻意**不进这条腿**:它的转录行是一行没有标签的散文,
|
|
326
|
+
* 要认它只能去锚 `[Cross-session …]` 前缀 —— 那正是本仓禁的那一形(用户随手打一行同样的字
|
|
327
|
+
* 就会被认成引擎通知)。通知按普通文本行渲,本来就是 CC 形。
|
|
328
|
+
*/
|
|
329
|
+
function hasRawTag(tag, body) {
|
|
330
|
+
return new RegExp(`<(?=/?${tag}(?:[>\\s/]|$))`, 'i').test(body);
|
|
331
|
+
}
|
|
332
|
+
export function parsePeerFrameText(text) {
|
|
333
|
+
const a = AGENT_MESSAGE_RE.exec(text);
|
|
334
|
+
if (a !== null) {
|
|
335
|
+
const from = unattr(a[1] ?? '');
|
|
336
|
+
// 🔴 正文里出现**未拆火**的同名标签 ⇒ 这不是本渲染腿产出的字节(它拆火过了),而**贪婪**的
|
|
337
|
+
// `[\s\S]*` 会把「两封拼在一起」读成一封、把两封的正文合并、把两位署名合并成第一位 ——
|
|
338
|
+
// 异源对抗复审 r1 [high] 的第三形。整条退 null:落回普通文本行,一个字节都不丢。
|
|
339
|
+
if (from.length === 0 || hasRawTag(AGENT_MESSAGE_TAG, a[2] ?? ''))
|
|
340
|
+
return null;
|
|
341
|
+
return { lane: 'agent_message', from, body: unescapeEnvelopeTag(AGENT_MESSAGE_TAG, a[2] ?? '') };
|
|
342
|
+
}
|
|
343
|
+
const c = CROSS_SESSION_RE.exec(text);
|
|
344
|
+
if (c === null)
|
|
345
|
+
return null;
|
|
346
|
+
const from = unattr(c[1] ?? '');
|
|
347
|
+
if (from.length === 0 || hasRawTag(CROSS_SESSION_MESSAGE_TAG, c[6] ?? ''))
|
|
348
|
+
return null;
|
|
349
|
+
const fromSession = c[2] !== undefined ? unattr(c[2]) : undefined;
|
|
350
|
+
const fromName = c[3] !== undefined ? unattr(c[3]) : undefined;
|
|
351
|
+
const fromMode = PEER_MODE_CLASSES.find(m => m === c[4]);
|
|
352
|
+
const fromScope = c[5] !== undefined ? unattr(c[5]) : undefined;
|
|
353
|
+
return {
|
|
354
|
+
lane: 'cross_session_message',
|
|
355
|
+
from,
|
|
356
|
+
body: unescapeEnvelopeTag(CROSS_SESSION_MESSAGE_TAG, c[6] ?? ''),
|
|
357
|
+
...(fromSession !== undefined ? { fromSession } : {}),
|
|
358
|
+
...(fromName !== undefined ? { fromName } : {}),
|
|
359
|
+
...(fromMode !== undefined ? { fromMode } : {}),
|
|
360
|
+
...(fromScope !== undefined ? { fromScope } : {}),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* 端渲「@谁」时该用的那个名字 —— 判定在包,端不许自己排优先序。
|
|
365
|
+
* 跨会话:`fromName`(人取的名)优先于 `from`(地址);同进程:只有 `from`。
|
|
366
|
+
*/
|
|
367
|
+
export function peerFrameDisplayName(p) {
|
|
368
|
+
return p.lane === 'cross_session_message' && p.fromName !== undefined ? p.fromName : p.from;
|
|
369
|
+
}
|