@sema-agent/client-core 0.76.0 → 0.76.2

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.
Files changed (46) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +7 -1
  3. package/dist/adapt/arms.js +51 -48
  4. package/dist/adapt/ids.d.ts +29 -2
  5. package/dist/adapt/ids.js +29 -2
  6. package/dist/adapt/panelTasks.d.ts +4 -3
  7. package/dist/adapt/panelTasks.js +29 -11
  8. package/dist/adapt/textStream.js +5 -5
  9. package/dist/adapt/toolCards.js +2 -2
  10. package/dist/adapt/turnFlags.js +5 -5
  11. package/dist/adapt.d.ts +1 -1
  12. package/dist/adapt.js +8 -8
  13. package/dist/adapter/downstream/eventToSdkMessage.d.ts +12 -0
  14. package/dist/adapter/downstream/eventToSdkMessage.js +13 -20
  15. package/dist/adapter/runStream.js +14 -5
  16. package/dist/adapter/types.d.ts +12 -0
  17. package/dist/adapter/types.js +30 -0
  18. package/dist/approvalsStreamLiveCapability.js +15 -35
  19. package/dist/deviceExecutorManagementCapability.js +15 -35
  20. package/dist/engineCapReader.d.ts +77 -0
  21. package/dist/engineCapReader.js +87 -0
  22. package/dist/executionLaneCapability.js +15 -38
  23. package/dist/hitl/persistedRulesWire.d.ts +20 -0
  24. package/dist/hitl/persistedRulesWire.js +23 -0
  25. package/dist/index.d.ts +6 -0
  26. package/dist/index.js +22 -1
  27. package/dist/mcpLiveness.d.ts +179 -0
  28. package/dist/mcpLiveness.js +218 -0
  29. package/dist/mcpPanel.d.ts +17 -0
  30. package/dist/mcpPanel.js +21 -0
  31. package/dist/memoryComplianceCapability.d.ts +73 -0
  32. package/dist/memoryComplianceCapability.js +109 -0
  33. package/dist/memoryEntriesWire.d.ts +302 -0
  34. package/dist/memoryEntriesWire.js +595 -0
  35. package/dist/memoryOriginCapability.d.ts +68 -0
  36. package/dist/memoryOriginCapability.js +102 -0
  37. package/dist/peerLaneCapability.d.ts +62 -0
  38. package/dist/peerLaneCapability.js +100 -0
  39. package/dist/permissionRulesWriteCapability.d.ts +64 -0
  40. package/dist/permissionRulesWriteCapability.js +98 -0
  41. package/dist/selfOrchestrationDenial.js +7 -2
  42. package/dist/sqlEngineCapability.js +15 -35
  43. package/dist/webSearchBackendCapability.js +15 -35
  44. package/dist/writeProtectionCapability.js +15 -35
  45. package/docs/INTEGRATION-CLIENTS.md +107 -11
  46. package/package.json +1 -1
@@ -0,0 +1,218 @@
1
+ /**
2
+ * mcpLiveness.ts — 引擎托管的 MCP 服务器**活性观察**(`wiring_manifest.mcp[].liveness`)的三端共用
3
+ * **结构读口** + 三词闭集词表 + 腿级判词(CC-36,0.76.1)。引擎内核自 7.24.3 起铸这一格,服务端的转发层自 7.91.2 起把它送上 wire ——
4
+ * 更老的服务端上这一格**恒缺席**(读数 = `indeterminate`,正是缺席语义要兜的那一档,不是故障)。
5
+ *
6
+ * ── 这一格答的是哪一个问题 ────────────────────────────────────────────────────────────────────
7
+ * 「这台服务器**还够得着吗**」。引擎不为它发任何自己的流量 —— 每条记录都是那条腿**已经做过的工作**
8
+ * (拨号、re-dial、传输关闭、一次发现传输没了的请求)顺带盖的章,自带盖章时刻 `observedAt`。
9
+ * ⇒ 它是**每条腿的一张快照**,不是心跳、不是轮询、更不是「此刻」。
10
+ *
11
+ * 🔴 **同一行上的三个位各说各的,谁都不许顶替谁、不许互相校验、不许据其一推另一个**:
12
+ * · `status`(申报 / 连接时判决)—— 这条腿**拨号那一刻**的判词,引擎**有意冻结**它(服务器中途死了这一格
13
+ * 仍读 `connected`);
14
+ * · re-dial 回执的 `outcome`(动作判决)—— **一次动作**的判词,过去式;
15
+ * · `liveness`(本件)—— 连接层**最后学到的那件事**,自带时戳。
16
+ * 最直白的一格:`status: 'failed'` 与 `liveness.state: 'reachable'` **同一行并存是真行** —— 服务器答了
17
+ * 握手、答的是协议错(**活着,且配错了**)。判据是「两格都在」,不是「两格一致」。
18
+ *
19
+ * ── 🔴 四种读数(三个词 + 缺席),缺席**不是**其中任何一个词 ────────────────────────────────────
20
+ * · `reachable` —— **一次交换完成了**(连上并列出了,或服务器自己答了 —— MCP 级的拒绝**也是答**,所以
21
+ * 它证明够得着,即便那次调用失败);
22
+ * · `unreachable` —— 试过,丢在传输或时钟上(拒连 / 重置 / 关闭 / 起不来 / 超时);
23
+ * · `unknown` —— **试过,而回来的东西答不了这个问题**(服务器前面那台网关写的 HTTP 状态、非 MCP 载荷、
24
+ * 没有结构的抛出值)。它是一句**更强**的断言:引擎看了,看不出来。
25
+ * · **缺席 = indeterminate** —— 这一行**没有可用的观察记录**。四个来源在 wire 上**同形**,消费端分不出也
26
+ * 不需要分:① 这条腿没走到那台服务器(`status: 'skipped'`);② 那份声明从来没被拨过(`invalid_config`);
27
+ * ③ 这是一条老引擎 / 老服务端的帧;④ 上游判形没过、那一格被摘掉了。
28
+ * 🔴 **不许据缺席反推「这条腿拨没拨过号」**,也不许把它折进 `unknown`、折成「健康」、折成「关着」。
29
+ * 这四句话在本包里是四句不同的话,任何折叠都是「把不知道说成没有 / 说成好着呢」的变体。
30
+ *
31
+ * ── 为什么是结构读(不靠型面) ─────────────────────────────────────────────────────────────────
32
+ * 本包 peer 地板上的 wire 型面里**还没有这一格**(键形是引擎侧先出、型面后出的常态)。所以本件按契约
33
+ * **结构读**:从 `unknown` 起按键路径窄化,判据自带(见下),**不抬 peer 地板、不等型面**。安全性不靠型
34
+ * 而靠三条:① 三词按**闭集恰等**判,认不得的词一律畸形(词表属主在上游,本包**不放宽**成开集 —— 放宽了
35
+ * 就会把一个没人定义过的词当成一句真话往屏上送);② `observedAt` 按**有限数**判;③ 判形没过 ⇒ 这一格
36
+ * 摘掉、那一行照读(与上游同判),而摘掉的**理由**留在 {@link McpLivenessCell} 的 `unreadable` 臂上,
37
+ * 与「引擎没报」分开。型面出键那天由包侧门逼一次显式处置(见 `run-mcp-liveness-test.mjs` 的上游见证臂)。
38
+ *
39
+ * ── `errorCode` 只随 `unreachable` ────────────────────────────────────────────────────────────
40
+ * 它是那句话的**注脚**(为什么够不着),不是那句话本身。上游的单点铸造保证它**只随 `unreachable`** 出现,
41
+ * 理由逐字可查:`unknown` 的全部意思就是「那次失败并没有决定可达性」,把失败类别挂在那个词上等于请消费端
42
+ * 拿一份说明不了可达性的证据去决定下一步动作。⇒ 本读口在**非 `unreachable`** 臂上**丢掉这只注脚**
43
+ * (整格仍在、`state` 仍读):去掉的恰是上游点名要防的那条误读路,而 `state` 那句话一个字没动。
44
+ * 注脚自己形坏(空串 / 非串)同样**只丢它自己不拆整格** —— 代价如实:此时读到的是「够不着,原因没说」,
45
+ * **不是**「这条记录坏了」。词按**开集**读(十词闭集的属主在上游,抄一张会把新词吞成缺席)。
46
+ */
47
+ /**
48
+ * 活性词的**三词闭集**(与上游同序:能完成交换 / 完不成 / 答不了)。
49
+ *
50
+ * 🔴 **它只有三个词**。腿级判词的第四个读数 `indeterminate`(见 {@link McpLegLivenessReading})**不是**
51
+ * 本表成员,也绝不许被加进来:本表是**上游的词表**,那第四个读数是**缺席**在本包里的名字。
52
+ * 🔴 消费端按本表写的 `switch` 判**恰等**(不 trim、不折大小写、不按前缀猜);上游加词的唯一处置是
53
+ * **本表同批加一格 + 补一条读数格**,而不是在消费点放宽判据。
54
+ */
55
+ export const MCP_LIVENESS_STATES = Object.freeze(['reachable', 'unreachable', 'unknown']);
56
+ /** 三词闭集的**唯一**判据(恰等;行级读口与腿级判词共用这一只,不各写一遍)。 */
57
+ const isLivenessState = (v) => typeof v === 'string' && MCP_LIVENESS_STATES.includes(v);
58
+ const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
59
+ /**
60
+ * 从**一行** MCP 名册条目里读这一格(结构读;单一铸点,别在消费点再写一遍判据)。
61
+ *
62
+ * 缺席答什么:`{kind:'absent'}` —— 判据是**自有键在不在**(`Object.hasOwn`),不按真值判。原型链上的
63
+ * 同名键**永远不会**上 wire,拿它当在场 = 把本进程对象的形状当成引擎说过的话;`null` / `undefined`
64
+ * 显式在场是**坏形**(走 `unreadable`),不是缺席形。
65
+ * 畸形答什么:`{kind:'unreadable'}` —— 整行不是对象、整格不是对象(含 `null` / 数组 / 串 / 数)、
66
+ * `state` 不是三词之一(含空串、近形拼法、大小写变体)、`observedAt` 非有限数(含缺席 / 串 / `NaN`)。
67
+ * 🔴 **绝不**折成 `{kind:'absent'}`(会把「送来一份读不懂的」说成「引擎没报」),更**绝不**折成
68
+ * `reachable` 或任何一个词(那是把读不出说成好着呢)。
69
+ * 为什么不折:这三句话在本包下游各对一种不同的处置 —— 缺席 = 什么都不渲(**不许**渲「一台都够不着」也
70
+ * **不许**渲「都好着」);读不出 = 渲「这一格读不出」并让运维去看引擎版本与投影层;观察 = 渲那个词
71
+ * + 那个时刻。折任何一对,渲出来的都是一句谁都没说过的话。
72
+ *
73
+ * ⚠️ 本口**只读这一格**:同行的 `status` / `errorCode` / `delivered` 一个字节都不碰,也不拿它们互相校验
74
+ * (三个位各说各的,见文件顶注)。
75
+ */
76
+ export function readMcpLiveness(row) {
77
+ // 整行不是对象:说「引擎没报」就是替引擎发言 —— 我们连有没有这一行都没读出来。
78
+ if (!isRecord(row))
79
+ return { kind: 'unreadable' };
80
+ if (!Object.hasOwn(row, 'liveness'))
81
+ return { kind: 'absent' };
82
+ const cell = row.liveness;
83
+ if (!isRecord(cell))
84
+ return { kind: 'unreadable' };
85
+ const state = cell.state;
86
+ if (!isLivenessState(state))
87
+ return { kind: 'unreadable' };
88
+ const observedAt = cell.observedAt;
89
+ if (typeof observedAt !== 'number' || !Number.isFinite(observedAt))
90
+ return { kind: 'unreadable' };
91
+ const liveness = { state, observedAt };
92
+ // 注脚:只随 unreachable 收,且形坏只丢它自己(见文件顶注最后一段)。
93
+ if (state === 'unreachable' && typeof cell.errorCode === 'string' && cell.errorCode.length > 0) {
94
+ liveness.errorCode = cell.errorCode;
95
+ }
96
+ return { kind: 'observation', liveness };
97
+ }
98
+ /**
99
+ * 一条腿的名册 → 活性判词(**唯一铸点**:别在各端的状态行装配里另写一遍这套合成规则)。
100
+ *
101
+ * 合成规则 = **最坏事实优先**,不是多数票、不是取最新一格:
102
+ * ① 任一行 `unreachable` ⇒ `unreachable`(有一台**可证**打不通时说「够得着」,就是把坏消息折成健康);
103
+ * ② 否则任一行 `unknown` **或任一行的格读不出**(`livenessUnreadable`,或入参自带一格形坏的 `liveness`)
104
+ * ⇒ `unknown`。在**腿**这一层,`unknown` 的意思恰是
105
+ * 「有观察在场而答不了这个问题」,而一份读不懂的格**同样答不了** ⇒ 🔴 **读不出的格绝不作可达的证据**
106
+ * (否则「一行读不出 + 一行 reachable」会合成一句「这条腿够得着」,而那行读不出的格完全可能本来写着
107
+ * `unreachable`)。为什么答不了,由 {@link McpEngineLegHealth.unreadableCells} 分辨;
108
+ * ③ 否则任一行 `reachable` ⇒ `reachable`(其余行缺席 = 没观察,不是反证);
109
+ * ④ 否则 ⇒ `indeterminate`。
110
+ *
111
+ * 缺席答什么:`{reading:'indeterminate'}` —— 三种入参落这一档且**刻意同形**:段读不出(`undefined`)、
112
+ * 零申报(`[]`,这条腿一台服务器都没申报 ⇒ 没有服务器就没有观察)、每一行的活性格都缺席(老引擎 /
113
+ * 都没走到)。要分「一台都没申报」与「有服务器但没报活性」,问名册本身(行数)或在场判词,**不要**
114
+ * 让本口替那一问答话。
115
+ * 畸形答什么:整个入参不是数组 ⇒ `indeterminate`;行本身不是对象 ⇒ **那一行什么都不贡献**(连「有没有
116
+ * 这一格」都没读出来,不替它认领一格);行在而那一格形坏 ⇒ 按上面的 ② 算「读不出」。**永不抛**。
117
+ * 为什么不折:`indeterminate` 与三个词之间**没有**默认值可回落 —— 回落到 `reachable` 是谎、回落到
118
+ * `unreachable` 是恐慌、回落到 `unknown` 是替引擎认领一次它没做过的观察。
119
+ */
120
+ export function mcpLivenessRollupOf(rows) {
121
+ if (!Array.isArray(rows))
122
+ return { reading: 'indeterminate' };
123
+ let unreachable = false;
124
+ let unknown = false;
125
+ let reachable = false;
126
+ let unreadable = false;
127
+ // 按**词**分别记最新时刻:判词定下来之后只取那个词自己的(见 McpEngineLegHealth.observedAt 的理由)。
128
+ const newestByWord = {};
129
+ for (const row of rows) {
130
+ if (row === null || typeof row !== 'object')
131
+ continue;
132
+ if (row.livenessUnreadable === true)
133
+ unreadable = true;
134
+ const lv = row.liveness;
135
+ // `undefined` = 这一格不在 = 没有观察(JSON 上根本没有这一键):什么都不贡献,也不算「读不出」。
136
+ if (lv === undefined)
137
+ continue;
138
+ // 形再核一次(本口是公面,入参可能来自宿主自建管线而不是本包的读器):整格不是对象、词出闭集、
139
+ // 时刻非有限数 ⇒ 与 `livenessUnreadable` **同判**(算「在场而读不出」)。🔴 刻意不让它「什么都不贡献」:
140
+ // 那样一行读不懂的格会被另一行的 `reachable` 盖过去,合成一句「这条腿够得着」—— 正是本件要防的那一折。
141
+ if (lv === null || typeof lv !== 'object' || Array.isArray(lv)) {
142
+ unreadable = true;
143
+ continue;
144
+ }
145
+ const word = lv.state;
146
+ if (!isLivenessState(word)) {
147
+ unreadable = true;
148
+ continue;
149
+ }
150
+ if (typeof lv.observedAt !== 'number' || !Number.isFinite(lv.observedAt)) {
151
+ unreadable = true;
152
+ continue;
153
+ }
154
+ if (word === 'unreachable')
155
+ unreachable = true;
156
+ else if (word === 'unknown')
157
+ unknown = true;
158
+ else
159
+ reachable = true;
160
+ const seen = newestByWord[word];
161
+ if (seen === undefined || lv.observedAt > seen)
162
+ newestByWord[word] = lv.observedAt;
163
+ }
164
+ const reading = unreachable
165
+ ? 'unreachable'
166
+ : unknown || unreadable
167
+ ? 'unknown'
168
+ : reachable
169
+ ? 'reachable'
170
+ : 'indeterminate';
171
+ // 时刻与判词同源:`indeterminate` 没有时刻;`unknown` 若只由读不出的格决定,那个词一格可读观察都没有 ⇒ 同样没有时刻。
172
+ const at = reading === 'indeterminate' ? undefined : newestByWord[reading];
173
+ return {
174
+ reading,
175
+ ...(unreadable ? { unreadableCells: true } : {}),
176
+ ...(at !== undefined ? { observedAt: at } : {}),
177
+ };
178
+ }
179
+ /** 观察时刻的呈前写法:ISO(与本包其余时刻面同形)。🔴 **超出 `Date` 可表示区间的有限数原样印数字,
180
+ * 绝不抛、也绝不回落到一个编出来的时刻** —— 那一格是引擎给的读数,不是本包的时钟。 */
181
+ const epochLabel = (ms) => Math.abs(ms) <= 8.64e15 ? new Date(ms).toISOString() : String(ms);
182
+ /**
183
+ * 活性那一行措辞的**唯一铸点**(三端共用;别在各端的行装配里另写一遍)。
184
+ *
185
+ * 🔴 四句刻意逐字互异,且**没有一句**把 `indeterminate` 说成「都好着」或「一台都够不着」:
186
+ * ① `reachable` —— 上次交换完成了(这条腿的观察,带时刻);
187
+ * ② `unreachable` —— 至少一台**可证**够不着;
188
+ * ③ `unknown` —— 看过了,答不了(读不出的格同落这一档,附注多一句);
189
+ * ③′ `unknown` 且**只**由读不出的格决定(判词无时刻 ∧ `unreadableCells`)—— 另一句:记录在场而**本端读不出**。
190
+ * ③ 那句的主语是「回来的东西」(引擎看了、看不出来),③′ 的主语是本端的读器 —— 上游哪天加了第四个词,
191
+ * 老客户端落的正是 ③′;把它说成 ③ 等于替引擎认领一次它没做过的「看不出来」。
192
+ * ④ `indeterminate` —— **没报**:这条腿上没有可用的观察记录,**不武断咎为引擎版本**(四源同形)。
193
+ * 🔴 句子里**不出现**服务器台数与「健康 / 正常」这类词:台数在名册上,而「健康」是本格答不了的问题
194
+ * (它只答「还够得着吗」)。
195
+ * 🔴 句尾那个时刻**属于句子里那个判词**(见 {@link McpEngineLegHealth.observedAt}):说「至少有一台够不着」
196
+ * 时它是最新那次**够不着**的观察时刻,不是这条腿上任何一次观察的最大值 —— 否则一句旧故障的判词会跟上
197
+ * 一个属于别的词的新时刻,读起来像刚刚又确认了一次。时刻缺席时句子里就没有时刻,不编「刚刚」。
198
+ */
199
+ export function mcpEngineLegHealthDetail(health) {
200
+ if (health === undefined)
201
+ return 'mcp liveness not reported (no liveness observation is available to this client)';
202
+ const unreadable = health.unreadableCells === true ? ' (one or more liveness records could not be read)' : '';
203
+ const at = health.observedAt !== undefined ? `, last observed at ${epochLabel(health.observedAt)}` : '';
204
+ switch (health.reading) {
205
+ case 'reachable':
206
+ return `mcp liveness: an exchange completed for every server that was observed on this leg${at}${unreadable}`;
207
+ case 'unreachable':
208
+ return `mcp liveness: at least one server was observed unreachable on this leg${at}${unreadable}`;
209
+ case 'unknown':
210
+ // ③′:时刻缺席 ∧ 有读不出的格 ⇒ 这个 unknown 一格可读观察都没有(时刻与判词同源,见 McpEngineLegHealth.observedAt)。
211
+ if (health.observedAt === undefined && health.unreadableCells === true) {
212
+ return 'mcp liveness: liveness records are present on this leg, but this client could not read them';
213
+ }
214
+ return `mcp liveness: observed on this leg, but what came back does not answer whether the server can be reached${at}${unreadable}`;
215
+ default:
216
+ return `mcp liveness not reported (no observation record on this leg's roster)${unreadable}`;
217
+ }
218
+ }
@@ -36,6 +36,7 @@
36
36
  */
37
37
  import type { AgentClient } from '@sema-agent/sdk';
38
38
  import { type WiringManifestMcpEntryView } from './adapter/downstream/eventToSdkMessage.js';
39
+ import { type McpEngineLegHealth } from './mcpLiveness.js';
39
40
  /** 面板 `servers[]` 一行(sdk `McpServerStatus` 的窄读;开集键不透传,逐键挑)。 */
40
41
  export interface McpPanelServerView {
41
42
  /** 配置名(引擎产的标识,非用户内容)。 */
@@ -120,3 +121,19 @@ export declare function mcpPanelLastLegDetail(view: McpPanelView | undefined, op
120
121
  */
121
122
  export type McpEngineLegPresence = 'present' | 'absent' | 'unknown';
122
123
  export declare function mcpEngineLegPresence(view: McpPanelView | undefined): McpEngineLegPresence;
124
+ /**
125
+ * CC-36(0.76.1):**这条腿的 MCP 活性判词**,只从本包的面板视图求(不自拼第二份判据、不发第二次请求)。
126
+ * 合成规则、四个读数与「缺席 ≠ unknown」那几句话的单一铸点在 {@link mcpLivenessRollupOf};本口只做一件事 ——
127
+ * 把面板上**带活性格的那一面**交给它。
128
+ *
129
+ * 🔴 **活性只在「最近那条腿的名册」这一面上**(`lastLegMcp.mcp[]`)。面板自己的 `servers[]` 半场**没有**
130
+ * 这一格:那一面是**部署缺省场景**此刻按需物化出来的台账,它的投影是逐键挑的白名单,今天不带活性位 ——
131
+ * 所以本口读不到它**不是漏读,是那一面今天没有这句话**。哪天它带上了,处置是同批补一格读数 + 判据,
132
+ * 而**不是**拿这一面的词去顶另一面(两面合法可不同,视图上从来没有、也不会有「一致/不一致」位)。
133
+ * 🔴 **判词说的是「那条腿」,不是「此刻」**:`observedAt` 属于那条腿的观察时刻,与面板的 `asOf` 不同源、
134
+ * **不比**。壳渲这一行必须带腿的时刻,别渲成一个实时健康灯。
135
+ * 🔴 `view === undefined`(传输失败 / 体读不出 / 空 sessionId,三形同形)与「面板读到了但没有那条腿的
136
+ * 名册」**都答 `indeterminate`**:两者的共同点正是这句话本身 —— 本包**说不出**这条腿的活性。
137
+ * 要分辨「这次读没读到面板」问 {@link mcpPanelLastLegDetail} 的 `reachable` 位,**不要**让本口替它答。
138
+ */
139
+ export declare function mcpEngineLegHealthOf(view: McpPanelView | undefined): McpEngineLegHealth;
package/dist/mcpPanel.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { hostLog } from './host.js';
2
2
  import { projectMcpSection } from './adapter/downstream/eventToSdkMessage.js';
3
3
  import { capForDisplay } from './fleetTaskDesc.js';
4
+ import { mcpLivenessRollupOf } from './mcpLiveness.js';
4
5
  /** `error` 上屏前的封长(server 侧 `slice(200)`,同值)。 */
5
6
  const MCP_PANEL_ERROR_MAX = 200;
6
7
  /** 名字类词形的封长(与本包其余 detail 铸点同值同理由)。 */
@@ -140,3 +141,23 @@ export function mcpEngineLegPresence(view) {
140
141
  return 'present';
141
142
  return 'absent';
142
143
  }
144
+ /**
145
+ * CC-36(0.76.1):**这条腿的 MCP 活性判词**,只从本包的面板视图求(不自拼第二份判据、不发第二次请求)。
146
+ * 合成规则、四个读数与「缺席 ≠ unknown」那几句话的单一铸点在 {@link mcpLivenessRollupOf};本口只做一件事 ——
147
+ * 把面板上**带活性格的那一面**交给它。
148
+ *
149
+ * 🔴 **活性只在「最近那条腿的名册」这一面上**(`lastLegMcp.mcp[]`)。面板自己的 `servers[]` 半场**没有**
150
+ * 这一格:那一面是**部署缺省场景**此刻按需物化出来的台账,它的投影是逐键挑的白名单,今天不带活性位 ——
151
+ * 所以本口读不到它**不是漏读,是那一面今天没有这句话**。哪天它带上了,处置是同批补一格读数 + 判据,
152
+ * 而**不是**拿这一面的词去顶另一面(两面合法可不同,视图上从来没有、也不会有「一致/不一致」位)。
153
+ * 🔴 **判词说的是「那条腿」,不是「此刻」**:`observedAt` 属于那条腿的观察时刻,与面板的 `asOf` 不同源、
154
+ * **不比**。壳渲这一行必须带腿的时刻,别渲成一个实时健康灯。
155
+ * 🔴 `view === undefined`(传输失败 / 体读不出 / 空 sessionId,三形同形)与「面板读到了但没有那条腿的
156
+ * 名册」**都答 `indeterminate`**:两者的共同点正是这句话本身 —— 本包**说不出**这条腿的活性。
157
+ * 要分辨「这次读没读到面板」问 {@link mcpPanelLastLegDetail} 的 `reachable` 位,**不要**让本口替它答。
158
+ */
159
+ export function mcpEngineLegHealthOf(view) {
160
+ if (view === undefined)
161
+ return { reading: 'indeterminate' };
162
+ return mcpLivenessRollupOf(view.lastLegMcp?.mcp);
163
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * memoryComplianceCapability — `GET /v1/capabilities.memoryCompliance` 的三端共用读面(0.76.1 CC-97a;
3
+ * 与 sql / writeProtection / webSearch.backend / executionLane / approvalsStreamLive /
4
+ * deviceExecutor.management 诸只兄弟同一套四态词汇,四口走共用工厂 {@link createEngineCapReader})。
5
+ *
6
+ * ── 这一位答的是哪一个问题 ────────────────────────────────────────────────────────────────
7
+ * 「**出处问询 / 抹除**两口(`GET /v1/memory/entries/:entryId/provenance` ·
8
+ * `POST /v1/memory/erase`,operator 车道)在这台引擎上在不在」。引擎侧的谓词是**两项合取**:
9
+ * ① 合规面挂上了 —— 挂载期判后端自带引擎控制面归属,缺则整口不挂;
10
+ * ② 这台部署的 operator 名单**非空** —— 空名单下两口对任何身份恒 403。
11
+ * ⚠️ 位为真**只**保证「门能过 + 面在场」,**不**保证这一次调用成功:控制面账本损坏是**运行期**事实
12
+ * (typed 500),引擎刻意不把它编进部署级布尔。
13
+ * ⚠️ 与「治理携出 bundle」那一位**不同源**:两位的第一项查的是后端的**不同面**,一个部署可以有 bundle
14
+ * 复合面而没有控制面归属,反之亦然 —— 任一侧都不许拿来替另一侧作答(合成一位会对其中一半撒谎)。
15
+ * ⚠️ 与 `memoryOrigin`(外源标记人面三口)**同判据不同位**:两族今天查后端的同一个面,但它们是**两个
16
+ * 产品面**,一个部署可以只开其中一族。引擎刻意不合并,即使今天两位恒同值 ⇒ **本包也不合并**:
17
+ * 两只读器各持一张 per-baseUrl 表,一只收到读不懂的回体只删自己那一格
18
+ * ({@link observedMemoryOrigin} 的读数一字不变)。
19
+ *
20
+ * ── 🔴 读法 ─────────────────────────────────────────────────────────────────────────────────
21
+ * ① 引擎铸这一位时**恒在场**(一格布尔,不是条件在场)⇒ **键缺席 = 老引擎根本不报这一位** ⇒
22
+ * `not_reported`(判不出)。折成 `false` 就是替引擎断言「这台没有合规面」。
23
+ * ② `false` 是**明确的否** ⇒ `absent`。wire 上分不出「后端没有控制面归属」与「operator 名单为空」
24
+ * 两种成因(引擎的谓词是合取,回体只有一格布尔)⇒ 措辞**不推因由**。
25
+ * ③ 严格布尔;`null` / 串 / `0` / `1` / 数组 / 对象一律畸形 ⇒ 删格 ⇒ `unobserved`,🔴 绝不折 `absent`
26
+ * (读不懂与「明确的否」是两回事)。
27
+ * ③b caps 回体本身不是对象(`null` / 数组 / 串 / 数)⇒ 畸形删格,**不是** `not_reported`(那一态只说「这是
28
+ * 一份 caps,但没有这一格」)。
29
+ * ④ 判在场用 `hasOwn`:原型链上的同名键永远不会被序列化上 wire,把它读成在场就是凭空造一格读数。
30
+ * ⑤ 「两口能不能用」的判据归包:{@link memoryComplianceVerbsAvailable} —— 只有 `present` 才 `'yes'`,
31
+ * `absent` ⇒ `'no'`(明确的否),其余 ⇒ `'unknown'`(端按 501 试探,不预判)。
32
+ */
33
+ /**
34
+ * 记忆治理面缺席时两族(合规两口 / 外源三口)共用的 501 码 —— **本包唯一铸点**,
35
+ * {@link memoryComplianceDoctorDetail} 与 {@link import('./memoryOriginCapability.js')} 的措辞表都取这一个常量,
36
+ * 两处不许各写一遍字面(码字面漂移时只有一处要改)。
37
+ */
38
+ export declare const MEMORY_ENGINE_REQUIRED_CODE = "capability.memory_engine_required";
39
+ /** 四态读数。`unobserved` 由读口在这一格空缺时铸,不由投影铸。 */
40
+ export type MemoryComplianceReading = {
41
+ kind: 'unobserved';
42
+ }
43
+ /** 键缺席:老引擎不报这一位 ⇒ 判不出(**不是** `absent`)。 */
44
+ | {
45
+ kind: 'not_reported';
46
+ }
47
+ /** 位在场且为 `false`:这台部署上两口用不了(成因在 wire 上不可分)。 */
48
+ | {
49
+ kind: 'absent';
50
+ }
51
+ /** 位在场且为 `true`:门能过 + 面在场(不承诺这一次调用成功)。 */
52
+ | {
53
+ kind: 'present';
54
+ };
55
+ /** caps 回体 → 本格读数;畸形一律 `undefined`(= 这一格不写 ⇒ 读口答 `unobserved`)。 */
56
+ export declare function projectMemoryComplianceCapability(caps: unknown): MemoryComplianceReading | undefined;
57
+ /** 宿主 caps probe 的读面 tee 落点(与诸只兄弟并列)。绝不 throw;畸形 ⇒ 删格;`opts.generation` 关掉旧探测覆盖新读数的竞态。 */
58
+ export declare function noteEngineCapsForMemoryCompliance(baseUrl: string, caps: unknown, opts?: {
59
+ generation?: number;
60
+ }): void;
61
+ /** 本进程观测到的读数;这一格空缺 ⇒ `{kind:'unobserved'}`。 */
62
+ export declare function observedMemoryCompliance(baseUrl?: string | undefined): MemoryComplianceReading;
63
+ /**
64
+ * 「出处问询 / 抹除两口能不能用」的判据单源(三态):只有引擎明说在场才 `'yes'`;明说不在 ⇒ `'no'`;
65
+ * 老引擎不报 / 从没观测 ⇒ `'unknown'`(端按 501 试探,不预判,也不预先藏掉入口)。
66
+ */
67
+ export declare function memoryComplianceVerbsAvailable(reading: MemoryComplianceReading): 'yes' | 'no' | 'unknown';
68
+ /** doctor 那一行的 detail 串,唯一措辞真源。🔴 两种「读不出」的句子都不许暗示「这台没有合规面」。 */
69
+ export declare function memoryComplianceDoctorDetail(reading: MemoryComplianceReading): string;
70
+ /** 换代失效口(引擎温切成功后调):清成未观测。空串 ⇒ no-op;绝不 throw。 */
71
+ export declare function forgetMemoryComplianceReading(baseUrl: string | undefined): void;
72
+ /** 测试钩子。 */
73
+ export declare function __resetMemoryComplianceReadingsForTests(): void;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * memoryComplianceCapability — `GET /v1/capabilities.memoryCompliance` 的三端共用读面(0.76.1 CC-97a;
3
+ * 与 sql / writeProtection / webSearch.backend / executionLane / approvalsStreamLive /
4
+ * deviceExecutor.management 诸只兄弟同一套四态词汇,四口走共用工厂 {@link createEngineCapReader})。
5
+ *
6
+ * ── 这一位答的是哪一个问题 ────────────────────────────────────────────────────────────────
7
+ * 「**出处问询 / 抹除**两口(`GET /v1/memory/entries/:entryId/provenance` ·
8
+ * `POST /v1/memory/erase`,operator 车道)在这台引擎上在不在」。引擎侧的谓词是**两项合取**:
9
+ * ① 合规面挂上了 —— 挂载期判后端自带引擎控制面归属,缺则整口不挂;
10
+ * ② 这台部署的 operator 名单**非空** —— 空名单下两口对任何身份恒 403。
11
+ * ⚠️ 位为真**只**保证「门能过 + 面在场」,**不**保证这一次调用成功:控制面账本损坏是**运行期**事实
12
+ * (typed 500),引擎刻意不把它编进部署级布尔。
13
+ * ⚠️ 与「治理携出 bundle」那一位**不同源**:两位的第一项查的是后端的**不同面**,一个部署可以有 bundle
14
+ * 复合面而没有控制面归属,反之亦然 —— 任一侧都不许拿来替另一侧作答(合成一位会对其中一半撒谎)。
15
+ * ⚠️ 与 `memoryOrigin`(外源标记人面三口)**同判据不同位**:两族今天查后端的同一个面,但它们是**两个
16
+ * 产品面**,一个部署可以只开其中一族。引擎刻意不合并,即使今天两位恒同值 ⇒ **本包也不合并**:
17
+ * 两只读器各持一张 per-baseUrl 表,一只收到读不懂的回体只删自己那一格
18
+ * ({@link observedMemoryOrigin} 的读数一字不变)。
19
+ *
20
+ * ── 🔴 读法 ─────────────────────────────────────────────────────────────────────────────────
21
+ * ① 引擎铸这一位时**恒在场**(一格布尔,不是条件在场)⇒ **键缺席 = 老引擎根本不报这一位** ⇒
22
+ * `not_reported`(判不出)。折成 `false` 就是替引擎断言「这台没有合规面」。
23
+ * ② `false` 是**明确的否** ⇒ `absent`。wire 上分不出「后端没有控制面归属」与「operator 名单为空」
24
+ * 两种成因(引擎的谓词是合取,回体只有一格布尔)⇒ 措辞**不推因由**。
25
+ * ③ 严格布尔;`null` / 串 / `0` / `1` / 数组 / 对象一律畸形 ⇒ 删格 ⇒ `unobserved`,🔴 绝不折 `absent`
26
+ * (读不懂与「明确的否」是两回事)。
27
+ * ③b caps 回体本身不是对象(`null` / 数组 / 串 / 数)⇒ 畸形删格,**不是** `not_reported`(那一态只说「这是
28
+ * 一份 caps,但没有这一格」)。
29
+ * ④ 判在场用 `hasOwn`:原型链上的同名键永远不会被序列化上 wire,把它读成在场就是凭空造一格读数。
30
+ * ⑤ 「两口能不能用」的判据归包:{@link memoryComplianceVerbsAvailable} —— 只有 `present` 才 `'yes'`,
31
+ * `absent` ⇒ `'no'`(明确的否),其余 ⇒ `'unknown'`(端按 501 试探,不预判)。
32
+ */
33
+ import { engineWireTarget } from './engineWireTarget.js';
34
+ import { createEngineCapReader } from './engineCapReader.js';
35
+ /**
36
+ * 记忆治理面缺席时两族(合规两口 / 外源三口)共用的 501 码 —— **本包唯一铸点**,
37
+ * {@link memoryComplianceDoctorDetail} 与 {@link import('./memoryOriginCapability.js')} 的措辞表都取这一个常量,
38
+ * 两处不许各写一遍字面(码字面漂移时只有一处要改)。
39
+ */
40
+ export const MEMORY_ENGINE_REQUIRED_CODE = 'capability.memory_engine_required';
41
+ /** caps 回体 → 本格读数;畸形一律 `undefined`(= 这一格不写 ⇒ 读口答 `unobserved`)。 */
42
+ export function projectMemoryComplianceCapability(caps) {
43
+ // 🔴 数组也是畸形:一个数组回体**不是**引擎对 caps 的回答,从它上面读出「老引擎不报这一位」是替引擎
44
+ // 编了一句它没说的话。`not_reported` 只留给**真的是 caps 对象、但这一格不在**那一形。
45
+ if (caps === null || typeof caps !== 'object' || Array.isArray(caps))
46
+ return undefined;
47
+ const c = caps;
48
+ if (!Object.hasOwn(c, 'memoryCompliance'))
49
+ return { kind: 'not_reported' };
50
+ // 🔴 只读一次:校验与分臂若各读一次,变化的 getter 能让校验读到布尔、分臂读到别的东西。
51
+ const v = c.memoryCompliance;
52
+ if (v === undefined)
53
+ return { kind: 'not_reported' };
54
+ if (typeof v !== 'boolean')
55
+ return undefined;
56
+ return v ? { kind: 'present' } : { kind: 'absent' };
57
+ }
58
+ /**
59
+ * 本格的 per-baseUrl 读账 + tee / 读口 / 失效口 / 测试钩四口 —— 能力位读器**共用同一份实现**
60
+ * ({@link createEngineCapReader});本文件只留这一格真正不同的部分:投影函数
61
+ * {@link projectMemoryComplianceCapability}、doctor 措辞表与本位的判据助手。
62
+ * 🔴 Map 在工厂闭包里,**一只读器一张**:收到读不懂的回体只删自己这一格,绝不连坐别的能力面
63
+ * (尤其不连坐同判据的 `memoryOrigin`)。
64
+ */
65
+ const reader = createEngineCapReader({
66
+ name: 'memoryCompliance',
67
+ project: projectMemoryComplianceCapability,
68
+ makeUnobserved: () => ({ kind: 'unobserved' }),
69
+ });
70
+ /** 宿主 caps probe 的读面 tee 落点(与诸只兄弟并列)。绝不 throw;畸形 ⇒ 删格;`opts.generation` 关掉旧探测覆盖新读数的竞态。 */
71
+ export function noteEngineCapsForMemoryCompliance(baseUrl, caps, opts) {
72
+ reader.note(baseUrl, caps, opts);
73
+ }
74
+ /** 本进程观测到的读数;这一格空缺 ⇒ `{kind:'unobserved'}`。 */
75
+ export function observedMemoryCompliance(baseUrl = engineWireTarget()?.baseUrl) {
76
+ return reader.observed(baseUrl);
77
+ }
78
+ /**
79
+ * 「出处问询 / 抹除两口能不能用」的判据单源(三态):只有引擎明说在场才 `'yes'`;明说不在 ⇒ `'no'`;
80
+ * 老引擎不报 / 从没观测 ⇒ `'unknown'`(端按 501 试探,不预判,也不预先藏掉入口)。
81
+ */
82
+ export function memoryComplianceVerbsAvailable(reading) {
83
+ if (reading.kind === 'present')
84
+ return 'yes';
85
+ if (reading.kind === 'absent')
86
+ return 'no';
87
+ return 'unknown';
88
+ }
89
+ /** doctor 那一行的 detail 串,唯一措辞真源。🔴 两种「读不出」的句子都不许暗示「这台没有合规面」。 */
90
+ export function memoryComplianceDoctorDetail(reading) {
91
+ switch (reading.kind) {
92
+ case 'unobserved':
93
+ return 'memory compliance face not observed — the engine reports it on /v1/capabilities (memoryCompliance); this process has no usable capabilities reading cached for it (none received, or the last one was unreadable)';
94
+ case 'not_reported':
95
+ return 'memory compliance face not reported by this engine — only newer engines advertise it; this does not say whether the provenance and erasure endpoints exist, probe them directly';
96
+ case 'absent':
97
+ return `memory compliance face off on this deployment — the entry-provenance and erasure endpoints are unavailable (${MEMORY_ENGINE_REQUIRED_CODE}); the engine does not say which half is missing (a memory backend that does not own the engine control plane, or an empty operator roster), so do not guess`;
98
+ case 'present':
99
+ return 'memory compliance face on — the entry-provenance and erasure endpoints are mounted and the operator roster is non-empty on this engine; this says the door opens, not that any one call succeeds (a damaged control-plane ledger is a runtime fact and arrives as a typed 500)';
100
+ }
101
+ }
102
+ /** 换代失效口(引擎温切成功后调):清成未观测。空串 ⇒ no-op;绝不 throw。 */
103
+ export function forgetMemoryComplianceReading(baseUrl) {
104
+ reader.forget(baseUrl);
105
+ }
106
+ /** 测试钩子。 */
107
+ export function __resetMemoryComplianceReadingsForTests() {
108
+ reader.__resetForTests();
109
+ }