@sema-agent/client-core 0.48.0 → 0.50.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.
@@ -0,0 +1,323 @@
1
+ /**
2
+ * crashConverged.ts — `GET /v1/approvals` 的 additive 键 `crashConverged` 的**三端公共读面**
3
+ * (L-38;server 7.55.0 起在场,fixture 直证坐标 `dist/boot/coordinators.js` 的 `GET /v1/approvals`
4
+ * 分支 + `dist/approval-ask-audit-store.d.ts` 的 `CrashConvergedRow`)。
5
+ *
6
+ * ## 这一格是什么(先把语义说准,文案才有得写)
7
+ * local 引擎在**人还挂在审批门上**的时候崩了:那一条 ask 既没被批也没被拒,进程一死就成了孤儿。
8
+ * server 重启后把这些孤儿**收敛成 DENIED 同码**(`decision:'denied'` + `cause:'crashed_before_park'`),
9
+ * 并把收敛结果作为一条 `crashConverged` 行挂在同一个 `/v1/approvals` 信封上。
10
+ * 对端(TUI / web / desktop)而言这是**上一条命的残留**:重连 / `--resume` 之后,人有权知道
11
+ * 「上次那次崩溃,把哪些审批替我拒掉了、其中哪几件可能已经落了一半副作用」。
12
+ *
13
+ * ## 两个桶的语义(端**唯一**需要分的那一刀)
14
+ * · `orphanState === 'pending'` = 崩的时候**工具一步都没执行**(还停在门上)。这一档配
15
+ * `resumeSafe === true` 时,重跑同一件事是安全的 ⇒ **resumeSafe 桶**。
16
+ * · `orphanState === 'decided'` = 人**当时已经批了 approve**,收敛把它翻成了 denied ——
17
+ * 可能有半截副作用落地了(文件写了一半 / 命令跑了一半)。⇒ **needsHuman 桶**,人工确认。
18
+ * · 其余一切(`resumeSafe === false`、字段自相矛盾、说不清的组合)一律进 **needsHuman**:
19
+ * 判**不**安全的代价是多问人一句(可恢复);判**错**安全的代价是让人闭眼重跑一件已经落过
20
+ * 副作用的事(不可恢复)。凡证不出来一律落保守侧。
21
+ *
22
+ * ## 🔴 caveat(DEBTS L-38 收执逐字,写文案前必读)
23
+ * `resumeSafe` 是**以账本完整为前提**算出来的**缺省值**,不是铁证 —— 崩溃现场本来就是账本最可能
24
+ * 缺页的时刻。⇒ 端的文案只许写「按记录看可以重跑」,**绝不**写「已确认没有副作用」。
25
+ * 同理:**键在场 ≠ 流内协议上场**。`crashConverged` 只是这一次 `list()` 回体上的一个 additive 键,
26
+ * 它既不宣示引擎具备什么能力,也不代表有一条推送通道会再告诉端第二次。别拿它当能力位读。
27
+ *
28
+ * ## 🔴 非目标与已知边界(对手模型成文,详见 `docs/INTEGRATION-CLIENTS.md` §12e)
29
+ * 真供给 = server JSON → SDK `JSON.parse` → 端:每一位都是**自有数据属性**,无代理、无 accessor、
30
+ * 原型恒 `Object.prototype`。**被中间层合成的非 JSON 载荷 / 敌意 `Proxy` / 原型注射**这一族**不在
31
+ * 射程内**:本件对它们只承诺三件 —— **不抛**、**不同步阻塞**、**绝不产出「可安全重跑」这个判决**
32
+ * (一律落 needsHuman 或 dropped);**不承诺**还原出「真实内容到底是什么」。一只代理在它唯一那次
33
+ * 被观察时就能给出假答案,而那与「宿主注入了一个会撒谎的传输层」是同一件事 —— 那种进程里每个对象
34
+ * 都不可信,本件不是能修好它的那一层。下面那一串防御的**唯一**目的是把这一族挡在「安全」判决之外
35
+ * 并保住线程,不是为了在敌意宿主上还原真相。
36
+ *
37
+ * ## 分工(与仓内既有形同款)
38
+ * **纯投影,零 IO、零 module 级可变态、零文案**:本件不构造 client、不认 baseUrl、不碰凭据,
39
+ * 也**一句面向用户的话都不铸** —— 措辞、是否上屏、排序与折叠全归端。
40
+ * 取件仍走既有的权威 `client.approvals.list()`(见 `hitlBridge.ts` 的 `ApprovalsResourceLike`),
41
+ * 本件只吃它的回体。
42
+ */
43
+ /**
44
+ * 🔴 **校验的尺子 = 类型面地板,不是「数据好不好看」**(L-38 复审采纳)。
45
+ * 本文件产出的行被声明成 {@link CrashConvergedRow},所以**类型对不上必须丢**(放过去就是在类型面
46
+ * 撒谎,端读到 `undefined` 时编译器不会提醒任何人)。但**超出类型面的严格**是另一回事:
47
+ * `ts: NaN`、`toolName: ''` 这类**退化但合型**的值只是难看,而为它们丢掉整行的代价是
48
+ * **一条真孤儿审批从人眼前消失** —— 那正是本面存在要防的那件事。⇒ 只在两处越过类型面:
49
+ * · `approvalId` 要**非空**(它是行身份:重复取件时的去重键、端要引用的那个 id;空串让这一行
50
+ * 结构上不可用,而不只是难看);
51
+ * · 三个闭集判别式(`decision`/`cause`/`orphanState`)—— 对不上不是「退化」,是**别族的行**。
52
+ * 其余一律只判类型。`dropped` 因此只数「**类型面**读不出的行」,不数「数据不好看的行」。
53
+ */
54
+ const isString = (v) => typeof v === 'string';
55
+ const isNumber = (v) => typeof v === 'number';
56
+ /** 行身份位:空串既去不了重也引用不了,结构上不可用(唯一一处越过类型面的串判)。 */
57
+ const nonEmptyString = (v) => typeof v === 'string' && v.length > 0;
58
+ /**
59
+ * 从一个**不可信**对象上取一位:只认**自有数据描述符**的 `value`。
60
+ *
61
+ * 🔴 为什么不用 `obj[key]`(复审采纳,L-38):普通属性读取会 ① **执行** accessor,② 一路查到
62
+ * **原型**上去。执行意味着同步跑别人的代码 —— 而 `try/catch` 接得住「抛」,接不住「不返回」:
63
+ * 一只忙等 / 死循环的 getter 会把启动 / `--resume` 路的线程**永久**钉住,行数上限对它无效。
64
+ * ⇒ 三处不可信读取(信封的 `crashConverged`、载体的 `length`、载体的每个数字下标)统一走这里:
65
+ * accessor / 缺席 / 只挂在原型上的东西**一律当缺席**,一次别人的代码都不执行。
66
+ * 真供给来自 `JSON.parse`,每一位都是自有数据位 ⇒ 这条对真行零影响。
67
+ * ⚠️ `Object.getOwnPropertyDescriptor` 自己对代理会触发 trap(可能抛)⇒ 调用点都在保护块内。
68
+ */
69
+ const ownDataValue = (obj, key) => {
70
+ const d = Object.getOwnPropertyDescriptor(obj, key);
71
+ if (d === undefined || 'get' in d || 'set' in d)
72
+ return undefined;
73
+ return d.value;
74
+ };
75
+ /**
76
+ * 载体行数的**硬上限**(复审采纳,L-38)。`length` 只判「非负整数」是不够的:一个只装着一行、
77
+ * 却让 `length` trap 答十亿的数组代理能过掉那道判据,随后本函数**同步**空转十亿次 —— 浏览器 /
78
+ * TUI 主线程当场冻住,而这条面恰恰跑在**启动/`--resume` 路**上。
79
+ * 取值理由:`/v1/approvals` 是 per-scope 的挂起队列,真实供给远在这个数量级之下;
80
+ * 10 万行的遍历是毫秒级,而十亿行不是「多等一会」而是「回不来」。
81
+ * 超限 ⇒ 与「载体读不出」同一档(`undefined`,端零渲染):一个说不清有多少行的供给,
82
+ * 本包不假装数得清。
83
+ */
84
+ const MAX_CRASH_CONVERGED_ROWS = 100_000;
85
+ function readCrashConvergedRow(v) {
86
+ // 🔴 **载体形判也在 try 内**(复审采纳,L-38):`Array.isArray(v)` 对一只**已撤销**的 `Proxy`
87
+ // 会抛 —— 它此前在 try 之外,于是一行读不出的载荷会把异常一路交给**外层**的 catch,
88
+ // 整只投影退化成 `undefined`:一条坏行**连坐**抹掉同批全部真孤儿,而端还会把这个结果
89
+ // 读成「本部署没提供这个面」。坏行的正确归宿是 `dropped` 一格,不是整批消失。
90
+ // (`typeof` 自己不会抛;会抛的是 `Array.isArray` 与后面每一次取属性。)
91
+ // 🔴 取属性本身要设防:供给来自宿主注入的传输层,它完全可以是一个带抛错 getter 的对象或敌意
92
+ // `Proxy`(与 `classifyMemoryStatusFailure` 同一条理由)。读一下就抛 ⇒ 判这一行读不出,
93
+ // 而不是让整只投影向外 reject —— 调用方是照着「本函数不抛」写的。
94
+ try {
95
+ if (typeof v !== 'object' || v === null || Array.isArray(v))
96
+ return null;
97
+ // 🔴 **恰一次枚举**(见头注):`Object.getOwnPropertyDescriptors` 是本函数对这一行的
98
+ // **唯一**一次观察 —— 它一次性拿到键集 + 每一位的描述符,且**不触发任何 getter**。
99
+ // 🔴 判据与快照必须出自**同一次**枚举:上一版先用它判 accessor、再用 `{ ...v }` **重新枚举
100
+ // 一遍**取值,于是一只**不抛**的 `Proxy` 只要在第一次 `ownKeys` 里亮出
101
+ // `originalDecision:'approve'`(⇒ 判定为纯数据、`unstable=false`)、在第二次 `ownKeys` 里
102
+ // 把这个可配置位**省掉**,快照就成了「pending + resumeSafe 且无已批证据」⇒ 落进 resumeSafe。
103
+ // 两次独立观察之间的任何不一致都是攻击面;一次观察则没有「另一次」可以与之矛盾。
104
+ // (代理仍可能在这唯一一次观察里撒谎 —— 那等同于「宿主注入了会撒谎的传输层」,见 §12 边界。)
105
+ const descs = Object.getOwnPropertyDescriptors(v);
106
+ const o = Object.create(null);
107
+ // 🔴 **自带原型的行一律 `unstable`**(复审采纳,L-38):快照只枚举**自有**位,所以挂在
108
+ // 原型上的 `originalDecision` / `decidedAtMs` 进不了快照 —— 跨位矛盾闸于是看不见证据,
109
+ // 一个**普通、无代理、无 accessor、观察完全稳定**的对象
110
+ // (`Object.assign(Object.create({originalDecision:'approve'}), row)`)就能落进 resumeSafe。
111
+ // 这不在「宿主注入的传输层整体撒谎」那条边界之内:它没撒谎,是本包少看了一层。
112
+ // ⇒ 原型不是 `Object.prototype` / `null` 的行,一律只挡「安全」这一侧(落 needsHuman,不丢)。
113
+ // 真供给来自 `JSON.parse`,原型恒是 `Object.prototype` ⇒ 这条对真行零影响。
114
+ const proto = Object.getPrototypeOf(v);
115
+ let unstable = proto !== Object.prototype && proto !== null;
116
+ // 🔴 **校验用的字典是 null 原型**(复审采纳,L-38):下面每一次 `o.xxx` 都是一次属性查找,
117
+ // 普通 `{}` 的查找会**落到 `Object.prototype` 上**。如果本进程里那份原型被污染过
118
+ // ——例如 `Object.prototype.sessionId` 被装成一只 `delete this.originalDecision` 的 getter——
119
+ // 那么「校验可选位」这一步就会把**快照里的已批证据**抹掉,随后跨位矛盾闸看不见证据,
120
+ // 危险行落进 resumeSafe(实测修前如此)。null 原型的字典**没有可查找的上一层**,
121
+ // 这条路径按构造消失。
122
+ // 🔴 **落键一律走 `Object.defineProperty`,绝不用 `o[k] = …`**(复审采纳,L-38):
123
+ // `'__proto__'` 是一个**合法的自有可枚举键**(JSON 里就出得来),而普通赋值对它**不是存值**
124
+ // —— 它会调用 `Object.prototype.__proto__` 的 **setter**,把那个值装成快照的**原型**。
125
+ // 于是一行「自有位全是纯数据」(⇒ `unstable` 为假)的载荷,可以把一只带 `sessionId` getter 的
126
+ // 对象注射成快照的原型;下面校验可选位读 `o.sessionId` 时那只 getter 就跑起来,
127
+ // `delete this.originalDecision` 把**快照里**的已批证据抹掉 ⇒ 危险行落进 resumeSafe
128
+ // (实测修前 `resumeSafe=1`)。accessor 闸看不见它:抛错的那一位在**注射进来的原型**上,
129
+ // 不在被枚举的行上。
130
+ // `defineProperty` 不触发任何 setter ⇒ `__proto__` 老老实实变成一个自有数据位
131
+ // (它遮住原型上那个 accessor),快照的原型仍是 `Object.prototype`,additive 键照样保全。
132
+ const put = (k, value) => {
133
+ Object.defineProperty(o, k, { value, enumerable: true, writable: true, configurable: true });
134
+ };
135
+ for (const k of Object.keys(descs)) {
136
+ const d = descs[k];
137
+ if (d === undefined || d.enumerable !== true)
138
+ continue;
139
+ if ('get' in d || 'set' in d) {
140
+ // 🔴 **accessor 一律不执行**(复审采纳,L-38):`d.get.call(v)` 是**同步**执行别人的代码,
141
+ // 而 `catch` 只接得住「抛」,接不住「不返回」—— 一只死循环 / 忙等的 getter 会把
142
+ // 启动 / `--resume` 路的线程**永久**钉住,10 万行上限对这一形完全无效。
143
+ // ⇒ 这一位当**缺席**处理,并把整行标 `unstable`。后果分两档,都是可接受的一侧:
144
+ // · 缺的是**必填位** ⇒ 校验过不了 ⇒ 这一行计入 `dropped`(响亮,不是静默);
145
+ // · 缺的是**可选 / additive 位** ⇒ 行照留,只是永远拿不到「可安全重跑」这个判决。
146
+ // 真供给来自 `JSON.parse`,每一位都是数据描述符 ⇒ 这条对真行零影响。
147
+ unstable = true;
148
+ continue;
149
+ }
150
+ put(k, d.value);
151
+ }
152
+ // 行身份:唯一一处越过类型面的严格(空 id 结构上不可用,见 nonEmptyString 头注)。
153
+ if (!nonEmptyString(o.approvalId))
154
+ return null;
155
+ // 其余必填位:只判**类型**。退化但合型的值(空 toolName / NaN 时刻)难看归难看,
156
+ // 为它丢掉整行 = 让一条真孤儿从人眼前消失。
157
+ if (!isString(o.toolName))
158
+ return null;
159
+ if (!isString(o.taskId))
160
+ return null;
161
+ if (!isNumber(o.ts))
162
+ return null;
163
+ if (!isNumber(o.expiresAtMs))
164
+ return null;
165
+ if (!isNumber(o.convergedAtMs))
166
+ return null;
167
+ if (typeof o.resumeSafe !== 'boolean')
168
+ return null;
169
+ // 判别式三位:对不上 = 这不是本族的行(见 CrashConvergedRow 头注的闭集说明)。
170
+ if (o.decision !== 'denied')
171
+ return null;
172
+ if (o.cause !== 'crashed_before_park')
173
+ return null;
174
+ if (o.orphanState !== 'pending' && o.orphanState !== 'decided')
175
+ return null;
176
+ // 可选三位:缺席合法,在场必须**合型**(同上,不再加类型面之外的严格)。
177
+ if (o.sessionId !== undefined && !isString(o.sessionId))
178
+ return null;
179
+ if (o.originalDecision !== undefined && o.originalDecision !== 'approve')
180
+ return null;
181
+ if (o.decidedAtMs !== undefined && !isNumber(o.decidedAtMs))
182
+ return null;
183
+ // 🔴 交还的是**普通原型**的对象(端拿到的是一只正常对象:`hasOwnProperty` / `toString` 都在),
184
+ // 但它的每一位都逐字来自上面那份 null 原型快照 —— 校验与分桶都发生在快照上,
185
+ // 交付只是把同一批值换个原型装出去。落键仍走 `defineProperty`(`__proto__` 同理)。
186
+ const delivered = {};
187
+ for (const k of Object.keys(o)) {
188
+ Object.defineProperty(delivered, k, { value: o[k], enumerable: true, writable: true, configurable: true });
189
+ }
190
+ return {
191
+ row: delivered,
192
+ unstable,
193
+ orphanState: o.orphanState,
194
+ resumeSafe: o.resumeSafe,
195
+ hasApprovalEvidence: o.originalDecision !== undefined || o.decidedAtMs !== undefined,
196
+ };
197
+ }
198
+ catch {
199
+ // 敌意行(已撤销 Proxy / 抛错 getter):判**这一行**读不出,由调用方计入 `dropped`。
200
+ // 不向外抛,更不让它连坐同批的好行。
201
+ return null;
202
+ }
203
+ }
204
+ /**
205
+ * `GET /v1/approvals` 回体 → `crashConverged` 的**分桶投影**。纯函数,**永不抛**。
206
+ *
207
+ * ## 🔴 缺席 vs 空数组:两件不同的事,判据不许合流
208
+ * · **键缺席**(老 server / deps 不在场 / 读不动)⇒ 返回 `undefined` ——「本部署没告诉我这件事」。
209
+ * 端此时**零渲染**:绝不渲「0 个」「本次无崩溃遗留」之类的话,那是替 server 下一个它没说过的
210
+ * 断言([honest-absence-not-fabricated-zero])。
211
+ * · **键在场且是空数组** ⇒ 返回 `{ total: 0, resumeSafe: [], needsHuman: [], dropped: 0 }` ——
212
+ * 「server 明说:一条都没有」。这一档端**可以**渲「没有崩溃遗留」。
213
+ * ⇒ 两档的**返回形不同**(`undefined` vs 对象),端拿 `=== undefined` 一刀分开,不必读计数。
214
+ *
215
+ * ## 🔴 载体在场却不是数组 ⇒ 同样 `undefined`(**不**折成 `total:0`)
216
+ * 那是形漂了或中间层改写了,本包**读不出**这次的供给。折成 `{total:0}` 会让端渲出
217
+ * 「没有崩溃遗留」——在一条给人判断「能不能闭眼重跑」的面上,这是最坏方向的假断言。
218
+ * 两档合流的代价只是「都零渲染」,而分错的代价是一句用户会照着去操作的谎。
219
+ *
220
+ * ## 🔴 三处不可信读取只认**自有数据位**
221
+ * 信封的 `crashConverged`、载体的 `length`、载体的每个数字下标 —— 三处都走 `ownDataValue`:
222
+ * accessor / 缺席 / 只挂在原型上的东西一律**当缺席**,一次别人的代码都不执行(`catch` 接得住
223
+ * 「抛」,接不住「不返回」)。真供给来自 `JSON.parse`,每一位都是自有数据位 ⇒ 对真行零影响。
224
+ *
225
+ * ## 🔴 载体**读不出**(已撤销 `Proxy` / `length` 或描述符取值抛)⇒ 也是 `undefined`
226
+ * 触碰载体的每一处都在保护内 —— 连 `Array.isArray()` 自己都是(对已撤销的 `Proxy` 调用它直接抛)。
227
+ * 走到一半炸掉时刻意**不交还半程结果**:一个自己都知道不全的计数,拿去渲「上次崩溃影响了 N 件」
228
+ * 比不说话更坏。
229
+ * 🔴 遍历**按数字下标**,不用载体自己的迭代协议:自带 `Symbol.iterator` 覆盖的数组能一条都不产出
230
+ * (于是真孤儿被伪造成「server 明说一条都没有」),也能把危险行替换成安全行。下标读不问载体
231
+ * 「有哪些行」这个问题。⚠️ 边界:代理仍能在 `length`/下标 trap 上撒谎 —— 那等同于「宿主注入了
232
+ * 会撒谎的传输层」,本包挡不住也不假装挡得住;本条守的是协议面。
233
+ *
234
+ * ## 🔴 进 resumeSafe 桶的合取有**五项**(两项主判据 + 两项跨位自洽 + 一项载体自证)
235
+ * `orphanState==='pending'`(工具零执行)∧ `resumeSafe===true` ∧ `originalDecision` 缺席 ∧
236
+ * `decidedAtMs` 缺席 ∧ 这一行**不带 accessor**。中间两项是**矛盾闸**:`pending` 说「一步都没执行」,
237
+ * 而那两位是「人已经按下过 approve」的证据 —— 同时在场 = 自相矛盾的载荷。最后一项是**顺序重入闸**:
238
+ * 带 getter 的行能在展开过程中把证据位删掉(见行读口头注的实测)。三者都**只挡「安全」这一侧**,
239
+ * 一律落 needsHuman 而**不丢** —— 它们可能是真孤儿,而且恰恰是最该给人看的那几条。
240
+ *
241
+ * ## 🔴 行数硬上限
242
+ * `length` 超过 100000 ⇒ 与「载体读不出」同一档(`undefined`)。判在遍历**之前**,所以一个谎报
243
+ * 十亿的 `length` trap 连一次下标读都触发不了 —— 否则同步空转会把启动/`--resume` 路的主线程冻住。
244
+ *
245
+ * ## 🔴 入参是 `unknown`,不是「带一个可选 `crashConverged` 的对象」
246
+ * 写成 `{ crashConverged?: unknown }` 会造出一个 TypeScript **弱类型**(成员全可选):把真
247
+ * `client.approvals.list()` 的回体喂进来时,SDK 7.4.0 声明的 `{ pending; livePending? }` 与它
248
+ * **一个共同属性都没有** ⇒ TS2559「has no properties in common」,文档里那句最主要的用法当场编不过
249
+ * (而只用本包自己的 `ApprovalsListEnvelope` 去测发现不了 —— 那个形恰好声明了 `crashConverged`)。
250
+ * 收 `unknown` 也更诚实:本函数对入参的全部工作**就是**窄化它,形状是运行期才知道的事。
251
+ *
252
+ * @param env 任意 `/v1/approvals` 回体(只读 `crashConverged` 一键;非对象 / `null` ⇒ `undefined`)。
253
+ */
254
+ export function projectCrashConverged(env) {
255
+ const resumeSafe = [];
256
+ const needsHuman = [];
257
+ let dropped = 0;
258
+ // 🔴 **整段读取都在保护内**(复审采纳,L-38):载体上的每一次触碰都可能执行**别人的**代码。
259
+ // ① 信封取属性(抛错 getter / `Proxy`);
260
+ // ② `Array.isArray()` 自己 —— 对一只**已撤销**的 `Proxy` 调用它直接抛 `TypeError`
261
+ // (它此前在 try 之外,那正是「永不抛」承诺上最后一个没堵的洞);
262
+ // ③ `length` 与逐个下标取值。
263
+ // 任一处抛 ⇒ 与「载体不是数组」同一档:这次供给**读不出** ⇒ 诚实缺席。
264
+ // ⚠️ 刻意**不**交还半程结果:走到一半才炸,已读到的行数说明不了总数,拿它去渲
265
+ // 「上次崩溃影响了 N 件」就是拿一个自己都知道不全的数当结论。
266
+ try {
267
+ if (typeof env !== 'object' || env === null)
268
+ return undefined;
269
+ // 🔴 信封这一位也只认**自有数据描述符**(见 `ownDataValue` 头注):普通读取会执行 accessor,
270
+ // 而一只不返回的 getter 挡不住 —— `catch` 接不住「不返回」。
271
+ const raw = ownDataValue(env, 'crashConverged');
272
+ if (raw === undefined)
273
+ return undefined;
274
+ if (!Array.isArray(raw))
275
+ return undefined;
276
+ // 🔴 **按数字下标走,不用载体自己的迭代协议**(复审采纳,L-38):`for…of` 把「这个数组里
277
+ // 到底有哪些元素」这件事**交给载体自己回答** —— 一个自带 `Symbol.iterator` 覆盖的数组
278
+ // (中间层改写 / 反序列化器的产物都造得出)可以:一条都不产出 ⇒ 本函数答
279
+ // `{total:0}`,而端会把它读成「server 明说一条都没有」,一条真孤儿就此人间蒸发;
280
+ // 或者把一条 `decided` 的危险行换成一条 `pending/resumeSafe` 的安全行 ⇒ 直接误导重跑。
281
+ // 下标读**读不到**这两种伪造(它不问载体「你有几个元素」以外的任何问题)。
282
+ // ⚠️ 边界说清楚:一只**代理**仍然能在 `length` / 下标 trap 上撒谎 —— 那与「宿主注入了一个
283
+ // 会撒谎的传输层」是同一件事,本包挡不住也不假装挡得住。本条守的是**协议面**:
284
+ // 不把「有哪些行」的解释权交给载体的迭代器。
285
+ const len = ownDataValue(raw, 'length');
286
+ if (typeof len !== 'number' || !Number.isInteger(len) || len < 0)
287
+ return undefined;
288
+ // 🔴 行数硬上限(见 MAX_CRASH_CONVERGED_ROWS 头注):**先判再遍历** —— 判在循环之前,
289
+ // 所以一个谎报十亿的 `length` 连一次下标读都触发不了。
290
+ if (len > MAX_CRASH_CONVERGED_ROWS)
291
+ return undefined;
292
+ for (let i = 0; i < len; i++) {
293
+ const reading = readCrashConvergedRow(ownDataValue(raw, String(i)));
294
+ if (reading === null) {
295
+ dropped++;
296
+ continue;
297
+ }
298
+ const row = reading.row;
299
+ // 🔴 **保守侧的合取有四项**(复审采纳,L-38):前两项是主判据,后两项是
300
+ // **跨位自洽**——`orphanState:'pending'` 的含义是「崩的时候工具一步都没执行」,而
301
+ // `originalDecision`/`decidedAtMs` 是「人当时已经按下过 approve」的证据。两者同时在场
302
+ // 是**自相矛盾**的载荷(版本斜差 / 畸形体 / 中间层改写都造得出),而它落错方向的代价正是
303
+ // 本面要防的那件事:让人闭眼重跑一件**可能已经开始落副作用**的事。
304
+ // ⇒ 矛盾形不产出「安全」这个确定判决,落 needsHuman(与 `readCaptureOptOut` 的完整
305
+ // 真值表同一条纪律:不在契约表上的组合一律不给确定答案)。
306
+ // 🔴 **不丢**:它是一条真孤儿,而且恰恰是最该给人看的那一条 —— 丢掉它比渲错更坏。
307
+ // 🔴 第五项:`unstable`(这一行的载体带 accessor 或自带原型,见行读口头注)—— 同样只把它
308
+ // 挡在「安全」这一侧,不丢。
309
+ // 🔴 三个判据一律读 `reading` 而**不是** `row`(见 {@link CrashConvergedRowReading} 头注):
310
+ // 它们是校验那一刻定下来的值,回头再读一遍交付物等于开第二次观察窗口。
311
+ const consistentlyUnexecuted = reading.orphanState === 'pending' && !reading.hasApprovalEvidence;
312
+ if (!reading.unstable && consistentlyUnexecuted && reading.resumeSafe === true)
313
+ resumeSafe.push(row);
314
+ else
315
+ needsHuman.push(row);
316
+ }
317
+ }
318
+ catch {
319
+ // 敌意/坏载体(已撤销 Proxy / `length` 或下标取值抛):供给读不出 ⇒ 诚实缺席,不向外抛。
320
+ return undefined;
321
+ }
322
+ return { total: resumeSafe.length + needsHuman.length, resumeSafe, needsHuman, dropped };
323
+ }
@@ -66,6 +66,7 @@
66
66
  * backend supplies the SIGNAL; the shell owns the chrome.
67
67
  */
68
68
  import type { AgentEvent, ApprovalDecision, ApprovalStaleError, PendingCheckpoint, CheckpointGate, PlanReviewRequest, AssistantTaskStatus } from '@sema-agent/sdk';
69
+ import type { ApprovalsListEnvelope } from './crashConverged.js';
69
70
  /** durable `/decide` 腿的既有缺省拒因(不带归因时逐字不变 —— 0.27.0 及之前的 wire 字节)。 */
70
71
  export declare const DEFAULT_DENY_REASON = "The user rejected this tool use";
71
72
  /** server 两条腿共用的 reason 字符上限(超限 413,决断被打回)。 */
@@ -96,12 +97,15 @@ export type HitlCanUseToolFn<D extends HitlPermissionDecisionLike = HitlPermissi
96
97
  }, input: Record<string, unknown>, toolUseContext: never, assistantMessage: never, toolUseID: string, forceDecision?: D) => Promise<D>;
97
98
  export interface ApprovalsResourceLike {
98
99
  /** GET /v1/approvals — the rich decide-ready queue (approvals.ts:45). NEVER carries a capability token.
99
- * SDK ≥0.1.0([1908] 信封归一):wire 信封原样 `{ pending }`。 */
100
+ * SDK ≥0.1.0([1908] 信封归一):wire 信封原样 `{ pending }`。
101
+ * 🔴 L-38(server 7.55.0):回体是 {@link ApprovalsListEnvelope} —— `pending` 之外还可能带
102
+ * additive 的 `livePending` / `crashConverged`(按 deps 在场才发)。**放宽是 additive**:
103
+ * 老 mock 的 `{ pending }` 仍然可赋值,本文件与 `approvalsFeed.ts` 的 `.pending` 消费点
104
+ * 一个字节不动。`crashConverged` 的读法见 `hitl/crashConverged.ts` 的 `projectCrashConverged`
105
+ * —— 🔴 它是**上一条命的残留**读面,与本桥这条 D-1 取件路**互不相干**,别在这里顺手消费它。 */
100
106
  list(opts?: {
101
107
  signal?: AbortSignal;
102
- }): Promise<{
103
- pending: PendingCheckpoint[];
104
- }>;
108
+ }): Promise<ApprovalsListEnvelope>;
105
109
  /** POST /v1/approvals/:sessionId/decide — resolve THAT checkpoint; the resumed run continues its stream
106
110
  * (approvals.ts:52). NOT a submit → no retry (a decide must never double-act). */
107
111
  decide(sessionId: string, decision: ApprovalDecision, opts?: {
package/dist/index.d.ts CHANGED
@@ -173,6 +173,7 @@ export * from './rewindWireCaps.js';
173
173
  export * from './selfOrchestrationWireCaps.js';
174
174
  export * from './skillsWireCaps.js';
175
175
  export * from './ultracodeWireCaps.js';
176
+ export * from './selfOrchestrationDenial.js';
176
177
  export * from './webSearchWireCaps.js';
177
178
  export * from './liveModelCatalog.js';
178
179
  export * from './modelBudgetRule.js';
@@ -238,6 +239,7 @@ export * from './hitl/resumeRunningCard.js';
238
239
  export * from './hitl/persistedRulesWire.js';
239
240
  export * from './hitl/localAllowRule.js';
240
241
  export * from './hitl/approvalsFeed.js';
242
+ export * from './hitl/crashConverged.js';
241
243
  export * from './interactiveHalt.js';
242
244
  export * from './compensations.js';
243
245
  export * from './request/printNotification.js';
package/dist/index.js CHANGED
@@ -199,6 +199,13 @@ export * from './rewindWireCaps.js';
199
199
  export * from './selfOrchestrationWireCaps.js';
200
200
  export * from './skillsWireCaps.js';
201
201
  export * from './ultracodeWireCaps.js';
202
+ // ── S-81(server 7.57.0):selfOrchestration **被拒**的三端公共判定 ────────────────────────────
203
+ // 上面两条 stamp 腿(selfOrchestrationWireCaps / ultracodeWireCaps)只管「怎么把意图发出去」;
204
+ // 半配置多租户形态下 server 会把带这两个键的提交 501 拒掉,而「这一发是不是被那两个键拒的 /
205
+ // 要去掉哪两个键 / caps 上的闸怎么读」三处都是判定不是文案 —— 三端各写一遍必然各错一遍
206
+ // (按 status 分诊会把别的 `capability.*` 501 拖进重发臂;手写 delete 必漏掉 `settings.ultracode`
207
+ // 那一处;把 `workflowsGate` 的缺席读成「引擎说不行」= 替一台什么都没说的 server 下断言)。
208
+ export * from './selfOrchestrationDenial.js';
202
209
  export * from './webSearchWireCaps.js';
203
210
  // 模型面纯逻辑族
204
211
  export * from './liveModelCatalog.js';
@@ -383,6 +390,14 @@ export * from './hitl/localAllowRule.js';
383
390
  // B7 ③(census G20,**行为改动**不是搬迁):pending-approvals 推送 feed(stream 优先 / 断流回落
384
391
  // 轮询 / 定期再试)。🔴 它**不替换** D-1 的取件 —— 那三处必须继续走权威 `list()`(见文件头)。
385
392
  export * from './hitl/approvalsFeed.js';
393
+ // ── L-38:`/v1/approvals` additive 键 `crashConverged` 的读面 + 纯投影 ─────────────────────────
394
+ // local 引擎崩在审批门上时,那些孤儿 ask 被 server 重启后收敛成 DENIED 同码;这一键把「上一条命
395
+ // 留下了什么」交到端手上。收在库里的理由是**两处判定**三端各写一遍必然各错一遍:① **缺席 vs
396
+ // 空数组**是两件事(键缺席 ⇒ `undefined`,端零渲染;空数组 ⇒ server 明说「一条都没有」),折成
397
+ // 「0 个」就是替 server 下一个它没说过的断言;② **分桶只有一个合取**(零执行 ∧ 按记录看安全),
398
+ // decided 臂与任何说不清的组合一律落人工确认侧 —— 判错「安全」会让人闭眼重跑一件已经落过副作用
399
+ // 的事。🔴 `resumeSafe` 是以账本完整为前提的**缺省值不是铁证**,文案纪律见文件头 caveat。
400
+ export * from './hitl/crashConverged.js';
386
401
  // ── #363 件③(0.47.0):交互 Esc 的**停止判定**三端公共上收 ──────────────────────────────────
387
402
  // 「Esc ⇒ 先发 turn 级 halt;只有那一发连判决都拿不到、而屏上又确实挂着审批卡时,才升级成 run 级
388
403
  // cancel」——TUI/desktop/web 三端都会 Esc、都会撞同一个 parked 格,判定本该在库里。此前整条住在
@@ -50,7 +50,8 @@ export type RetryStatus =
50
50
  /** 见 {@link BrainStatusPayload.retryAtMs}(与 `stalled` 臂同义同纪律:在场优先于 `deadline`)。 */
51
51
  retryAtMs?: number;
52
52
  /** 见 {@link BrainStatusPayload.errorStatus}。CC parity:`system/api_retry.error_status`
53
- * 就是这个数,端可据它渲「API Error 529 · Retrying」这类**点名失败方**的行。 */
53
+ * 就是这个数,端可据它渲「API error 529」这类**点名失败方**的行(CC 语料直证该行是小写 `API error`
54
+ * 且不渲码;状态码是端的超集追加 —— 档 §3g 取证订正 2026-09-01)。 */
54
55
  errorStatus?: number;
55
56
  /**
56
57
  * 🔴 **终态位**(2026-08-08 对抗复审命中):`true` ⇔ 引擎**不会再重试了**(`gave_up` 相)。
@@ -0,0 +1,210 @@
1
+ /**
2
+ * selfOrchestrationDenial.ts — **「这台部署不给 selfOrchestration」的三端公共判定**
3
+ * (S-81;server 7.57.0 起在场)。
4
+ *
5
+ * ## 这一格是什么(先把上游事实说准,判据才有得写)
6
+ * server 7.57.0 在**半配置的多租户形态**(`REQUIRE_PRINCIPAL=true` + `SELF_ORCHESTRATION_ENABLED=true`
7
+ * + 没有中心侧的准入解析器)上收窄了三条:
8
+ * ① `GET /v1/capabilities` 的 `workflows` 由 `true` 变 **`false`** —— 之前它只回答「这个部署开没开
9
+ * workflow 引擎」,现在它同时把「本 principal 能不能真用上」算进去了;
10
+ * ② 同一份 caps 上新增 additive 键 `workflowsGate: { engineCan, denial }` —— 把上面那个合流的判断
11
+ * **拆回两根轴**:`engineCan` = 引擎侧开没开,`denial` = 为什么这位调用者仍然用不上
12
+ * (闭集,今天单成员 `entitlement_resolver_absent`);
13
+ * ③ `POST /v1/tasks`(及同闸的 stream 提交)带 `selfOrchestration:true` 或 `settings.ultracode:true`
14
+ * 在该形态下 ⇒ **501 `capability.self_orchestration_required`**;把这两个键去掉,同一条请求照常受理。
15
+ *
16
+ * ## 为什么这件在库里而不是在三端各写一遍
17
+ * 三处判定,三端各写一遍必然各错一遍:
18
+ * · **「501 要不要重发」是判定不是文案**:重发的前提是「这一发的失败原因恰好是这两个键」——
19
+ * 判据只能是 `status===501` **且** `errorCode` 恰等那一个码。按 status 分诊会把别的 501
20
+ * (别的能力位没接线)也拖进「去掉两个键再来一次」,那是替 server 编了一个它没说的原因。
21
+ * · **「去掉哪两个键」是结构操作不是措辞**:`selfOrchestration` 在**顶层**、`ultracode` 在
22
+ * `settings` 下(两条不同的 stamp 腿,见 `src/selfOrchestrationWireCaps.ts` /
23
+ * `src/ultracodeWireCaps.ts`),端各自手写 `delete` 必然有人漏掉第二个,而漏掉的后果是
24
+ * 「重发一次、又被 501 拒一次」——用户看到的是功能坏了两遍。
25
+ * · **caps 的「缺席」与「关着」是两件事**:老 server 压根没有 `workflowsGate` 这个键,把它读成
26
+ * 「引擎说不行」就是替一台什么都没说的 server 下断言([honest-absence-not-fabricated-zero])。
27
+ *
28
+ * ## 分工(与仓内既有形同款)
29
+ * **纯判定 + 纯结构操作,零 IO、零 module 级可变态、零文案**:本件不构造 client、不认 baseUrl、
30
+ * 不碰凭据,也**一句面向用户的话都不铸** —— 措辞、是否上屏、要不要给入口全归端(接线姿势见
31
+ * `docs/INTEGRATION-CLIENTS.md` §13)。
32
+ *
33
+ * ## 🔴 射程边界(与 `src/hitl/crashConverged.ts` §12e 同一条边界,别把它读成更强的话)
34
+ * 真供给 = server JSON → SDK `JSON.parse` → 端:每一位都是**自有数据属性**,无代理、无 accessor。
35
+ * 被中间层合成的非 JSON 载荷 / 敌意 `Proxy` / 原型注射这一族**不在射程内**:本件对它们只承诺
36
+ * **不抛**、**绝不把一个说不清的 `denial` 折成「没有拒绝」**;**不承诺**还原出「真实内容到底是什么」。
37
+ *
38
+ * 🔴 **「getter 零执行」这一条只对 {@link projectWorkflowsGate} 成立,对
39
+ * {@link classifySelfOrchestrationRefusal} 不成立**(异源对抗复审 [medium] 采纳的订正 ——
40
+ * 上一版把它写成整模块承诺,与实现不符,而一句做不到的承诺比没有承诺更坏):
41
+ * · `projectWorkflowsGate` 读的是 **wire JSON**(`/v1/capabilities` 回体),那里每一位按定义
42
+ * 都是自有数据位 ⇒ 只认自有数据描述符不损失任何真供给,accessor 一次都不执行;
43
+ * · `classifySelfOrchestrationRefusal` 读的是**抛出物**,而抛出物按设计可能是 SDK 的
44
+ * `APIError` **类实例** —— `status` / `errorCode` 完全可能坐在**原型**上、甚至是原型上的
45
+ * getter(传输层的写法本包不拥有)。只认自有数据描述符会把一个读得懂的错判成读不懂,
46
+ * 那正是它必须走**普通属性读取**的原因(与 `classifyMemoryStatusFailure` /
47
+ * `classifyRulesFailure` 逐字同款)。⇒ 它对**抛出物**只承诺「不抛」;一只**挂死**的 getter
48
+ * 长在抛出物上时本包挡不住,那与「宿主注入了一个会撒谎的传输层」是同一件事。
49
+ * · 能收窄的那一半已经收窄:`denial` 这一位**只在两条判据都通过之后**才读(此前无条件先读,
50
+ * 于是一个**根本不匹配**的错误也会被跑一次 getter)。
51
+ */
52
+ import type { TaskRequest } from '@sema-agent/sdk';
53
+ import type { TaskRequestLike } from './request/taskRequest.js';
54
+ /**
55
+ * 一次 501 拒绝上**读得出来的**机器原因。
56
+ *
57
+ * 🔴 `'unknown'` 是**诚实缺席**,不是「没有原因」:今天 server 的 501 体形与其它 `capability.*`
58
+ * 501 同款(`{ error, errorCode, message }`),**并不携带**机器可读的 `denial` 位 ⇒ 真 wire 上
59
+ * 这一位恒是 `'unknown'`。闭集那一臂是**防御臂**(server 补了这一位的当天自动点亮),留着的理由
60
+ * 与 `sessionMemoryStatus` 的 `feature` 臂逐字同款:今天不可达 ≠ 缺口,而是「不合流」的登记。
61
+ * 端要给出具体原因时,材料在 **caps 侧**({@link projectWorkflowsGate} 的 `denial`),不在这条错误上。
62
+ */
63
+ export type SelfOrchestrationDenialReason = 'entitlement_resolver_absent' | 'unknown';
64
+ /**
65
+ * 重发前必须**同时**去掉的两个意图键(单源;两条 stamp 腿各在一个文件里,端手抄必漏第二个)。
66
+ * 顺序即书写顺序,`settings.ultracode` 用点分路径表达「它在 `settings` 下,不是顶层键」。
67
+ * 真正执行删除的是 {@link stripSelfOrchestrationIntent} —— 本常量是给端做披露文案/日志用的**清单**,
68
+ * 不是让端自己照着 `delete` 一遍(那正是本模块要根除的散抄病)。
69
+ *
70
+ * 🔴 **运行期冻结,不只是 `as const`**(异源对抗复审 [medium] 采纳):`as const` 只给**编译期**
71
+ * 只读性,运行期这只数组照样可写 —— 而 {@link classifySelfOrchestrationRefusal} 把**同一只
72
+ * 引用**当作判决的 `retryWithout` 带出去(单源的代价)。于是任意一个 JS 消费者(或一次
73
+ * `as unknown as string[]` 强转)`splice` 它一下,**此后同进程内每一次判决**都会带着被改写的
74
+ * 清单 —— 实测复现:`m.SELF_ORCHESTRATION_RETRY_WITHOUT.splice(0, 2, 'objective')` 之后,
75
+ * 判决的 `retryWithout` 变成 `['objective']`,端照它去键就会删掉 `objective`。
76
+ * `Object.freeze` 让写入在严格模式(ESM 恒是)下当场抛、在非严格下静默失败,两条路都改不动它。
77
+ * 与 `RUN_LEVEL_STOP_ERROR_CODES` 同款写法(那一处也是「导出的闭集表必须真冻」)。
78
+ */
79
+ export declare const SELF_ORCHESTRATION_RETRY_WITHOUT: readonly ['selfOrchestration', 'settings.ultracode'];
80
+ /**
81
+ * {@link classifySelfOrchestrationRefusal} 的判决。**只有一种 kind** —— 本判定回答的是一个是非题
82
+ * (「这一发是不是被那两个键拒的」),不是分类题;认不出来一律 `null`,不给第二个 kind 去承接
83
+ * 「大概是吧」。
84
+ */
85
+ export interface SelfOrchestrationRefusal {
86
+ readonly kind: 'self-orchestration-denied';
87
+ /** 端重发前要去掉的键清单(= {@link SELF_ORCHESTRATION_RETRY_WITHOUT},随判决带出便于披露)。 */
88
+ readonly retryWithout: typeof SELF_ORCHESTRATION_RETRY_WITHOUT;
89
+ /** 这次拒绝上读得出来的机器原因;读不出 ⇒ `'unknown'`(见 {@link SelfOrchestrationDenialReason})。 */
90
+ readonly reason: SelfOrchestrationDenialReason;
91
+ }
92
+ /**
93
+ * 一次任务提交失败(任意抛出物)→ 「这是不是 selfOrchestration 准入拒绝」。**永不抛**。
94
+ *
95
+ * 🔴 **两条判据是合取,且都不许放宽**:
96
+ * · `status === 501` —— 别的状态码一律 `null`(400 是「键的值不对」、403 是「越权」,
97
+ * 两者都不该靠去掉键来重试);
98
+ * · `errorCode` **恰等** {@link CAPABILITY_SELF_ORCHESTRATION_REQUIRED} —— 不是 `capability.`
99
+ * 前缀判。这个码是**复用码**,与它同前缀的兄弟(别的能力位没接线)去掉这两个键**也不会**变成
100
+ * 可受理,把它们一起拖进重发臂 = 白发一次请求 + 给用户一句错误的原因。
101
+ * · 两者**都**要在场:无码的 501 判不出(如实 `null`,绝不挑一个猜);码对但状态码不是 501
102
+ * 同样 `null`(那不是本闸说的话)。
103
+ *
104
+ * 🔴 **结构视图读,不 `instanceof`**(与 `classifyMemoryStatusFailure` / `classifyRulesFailure`
105
+ * 逐字同因):抛出来的是不是 SDK 的 `APIError` 由**宿主**决定 —— 跨 realm / 双 SDK 实例下
106
+ * `instanceof` 会把一个读得懂的错判成读不懂。SDK 的 `APIError` 形与端自己合成的
107
+ * `{ status, errorCode }` 裸形因此走**同一条**读法。
108
+ *
109
+ * 🔴 **取属性本身要设防**:`e` 是任意抛出物,可以是带抛错 getter 的对象或敌意 `Proxy`,
110
+ * 那样连 `e.status` 这一下都会抛。本函数常常在 `catch` 块里被调用,而 `catch` 内抛出的异常
111
+ * **不会**再被同一个 `try` 接住 —— 分类器一抛,调用方那句「永不抛」当场破功。
112
+ *
113
+ * 🔴 **本函数走普通属性读取(会沿原型、会执行 getter),这是刻意的**,理由与边界见文件头
114
+ * 「getter 零执行只对 `projectWorkflowsGate` 成立」那一段:抛出物可能是类实例,判据位坐在原型上。
115
+ * 能收窄的那一半已经收窄 —— `denial` **只在 501 ∧ 恰码两条判据都通过之后**才读(异源对抗复审
116
+ * [medium] 采纳):一个根本不匹配的错误不该被本函数跑一次它的 getter,而 `status`/`errorCode`
117
+ * 两位是判据本身,没有更早的地方可以躲。
118
+ *
119
+ * @returns 认得 ⇒ 判决(端据此去键重发**一次**,见档 §13);认不得 ⇒ `null`(按普通失败呈现)。
120
+ */
121
+ export declare function classifySelfOrchestrationRefusal(e: unknown): SelfOrchestrationRefusal | null;
122
+ /**
123
+ * 把一份已装配好的请求体里**两个 selfOrchestration 意图键**去掉,返回**新对象**。
124
+ *
125
+ * 删的**恰好**是这两处,一个字节都不多动:
126
+ * · 顶层 `selfOrchestration`(`selfOrchestrationFromEnv()` 的 stamp 位);
127
+ * · `settings.ultracode`(`ultracodeForRequest()` 的 stamp 位)。
128
+ * 🔴 `settings` 下的**其它键一个都不碰**(`webSearch` / `hooks` / `outputStyle` / `resolved`
129
+ * 摊开的那一片子键都是别的轴,连坐删掉 = 用一次重试静默改掉用户的其它设置);
130
+ * 只有当 `ultracode` **本来在场**、删完之后 `settings` 里**一个自有可枚举键都不剩**时,才把
131
+ * `settings` 整键删掉(空 `settings` 上 wire 是多余字节,与 `buildTaskRequest`「全缺则整个
132
+ * `settings` 键都不出现」的既有语义一致)。
133
+ *
134
+ * 🔴 **幂等**:再调一次得到同形结果(第一次之后两个键都不在了,两条分支都成了空操作)。
135
+ * 🔴 **不动 `deferTools`**:`workflowDeferForRequest` 的前置门是「stamp 在场」,而本函数的调用点
136
+ * 是**重发**——把 `Workflow` 从 deferTools 里拿掉或塞进去都是行为改动,不是「去掉意图」。
137
+ * 这一条刻意留给端与 `buildTaskRequest`,本函数只做减法、且只减这两处。
138
+ * 🔴 **additive 键全保**:顶层与 `settings` 都按「拷全部自有可枚举键、再删点名的那一个」做,
139
+ * 上游明天加的键照样过境(与 `run-additive-key-passthrough-test.mjs` 同一条纪律)。
140
+ * 🔴 `settings` **没被改动时原样带出**(同一只对象):没有改动就不该产生新的字节,
141
+ * 也让「我到底改了什么」在端侧一眼可判。
142
+ *
143
+ * 🔴 **两个重载,不是一个**(异源对抗复审 R3 [medium] 采纳):只留宽形
144
+ * (`TaskRequestLike` = `Record<string, unknown>`)会**擦掉**调用方的类型 —— 一份 SDK `TaskRequest`
145
+ * 进去、出来就不再可赋回 `TaskRequest`(索引签名下 `objective` 是 `unknown`,必填位的保证没了),
146
+ * 于是档里那句「去键之后直接重发」在 TypeScript 上根本编不过,端只能靠 `as` 强转把类型面绕开。
147
+ * ⇒ 第一重载**收窄到 SDK 的 `TaskRequest`**:本函数删的两位(`selfOrchestration` 顶层键、
148
+ * `settings.ultracode`)在那个型里**都是可选位**,所以「删完仍是一份合法 `TaskRequest`」是
149
+ * 类型面上成立的事实,不是宽容。第二重载保留给本包自己的 `buildTaskRequest` 产物(宽形)。
150
+ * 🔴 刻意**不**写成 `<T extends TaskRequestLike>(req: T): T`:那对一个把
151
+ * `selfOrchestration` 推断成**必填**的对象字面量就是类型面撒谎(键真的被删了)。
152
+ *
153
+ * @param req 已装配好的请求体(SDK `TaskRequest`,或 `buildTaskRequest` 的产物形)。本函数不校验
154
+ * 它的形状 —— 它是调用方手上已经成形的请求,不是不可信供给。
155
+ */
156
+ export declare function stripSelfOrchestrationIntent(req: TaskRequest): TaskRequest;
157
+ export declare function stripSelfOrchestrationIntent(req: TaskRequestLike): TaskRequestLike;
158
+ /**
159
+ * `workflowsGate.denial` 上一个**本包不认得**的值。带出原串是为了让端能把它记进日志/诊断面 ——
160
+ * 端**不许**拿它当文案直接上屏(那是 server 的内部词,不是给人看的话),更不许因为「认不得」
161
+ * 就当作没有拒绝。
162
+ *
163
+ * 🔴 `unknown` 为空串 = 「闸确实说了点什么,但那个值连一个可读的记号都取不出来」
164
+ * (非串的值 / accessor 位)。它**仍然是拒绝**,与 `null`(闸明说没有拒绝)是两件事。
165
+ */
166
+ export interface WorkflowsGateUnknownDenial {
167
+ readonly unknown: string;
168
+ }
169
+ /**
170
+ * {@link projectWorkflowsGate} 的产出。
171
+ *
172
+ * 🔴 三位各答一个不同的问题,端**不许**把它们合流:
173
+ * · `workflows` —— server 合流后的结论:「这位调用者现在能不能用 workflow」(7.57.0 起它已经
174
+ * 把准入算进去了)。这是**唯一**该拿来决定「给不给入口」的位。
175
+ * · `engineCan` —— 引擎侧开没开(`undefined` = 本部署**没说** ⇒ 老 server / 闸读不出,端零渲染)。
176
+ * · `denial` —— 为什么这位调用者仍然用不上。`null` = 闸明说没有拒绝;闭集成员 = 认得的原因;
177
+ * {@link WorkflowsGateUnknownDenial} = 拒了但原因认不得(**绝不**折成 `null`)。
178
+ */
179
+ export interface WorkflowsGateProjection {
180
+ readonly workflows: boolean;
181
+ readonly engineCan: boolean | undefined;
182
+ readonly denial: 'entitlement_resolver_absent' | WorkflowsGateUnknownDenial | null;
183
+ }
184
+ /**
185
+ * `GET /v1/capabilities` 回体 → workflows 闸的**三位投影**。纯函数,**永不抛**。
186
+ *
187
+ * ## 🔴 缺席 vs 关着:两件不同的事,判据不许合流
188
+ * · `caps` 非对象 / `workflows` 位不是布尔(缺席、accessor、类型漂了)⇒ 返回 `undefined` ——
189
+ * 「本部署没告诉我这件事」。端此时**零渲染**:绝不渲「workflow 不可用」之类的话,那是替
190
+ * server 下一个它没说过的断言([honest-absence-not-fabricated-zero])。
191
+ * · `workflows` 是布尔而 `workflowsGate` 缺席(或在场却读不出)⇒
192
+ * `{ workflows, engineCan: undefined, denial: null }` —— 老 server 的真形:合流结论有,
193
+ * 两根轴没有。`engineCan === undefined` 就是端判「这台 server 没有这个闸」的那一位。
194
+ * ⇒ 两档的返回形不同(`undefined` vs 对象),端拿 `=== undefined` 一刀分开。
195
+ *
196
+ * ## 🔴 `denial` 的四档(**未知值绝不折成 `null`**)
197
+ * · 键缺席 / 值是 `null` 或 `undefined` ⇒ `null`(闸明说没有拒绝);
198
+ * · 恰等闭集成员 ⇒ 该字面量(端可以据此说人话);
199
+ * · **别的串** ⇒ `{ unknown: <该串> }` —— server 加了第二个成员而本包还没跟车。折成 `null`
200
+ * 会让端渲出「没有任何拒绝」,而真相是「拒了,只是我不认得原因」:那是这条面上最坏方向的
201
+ * 假断言(用户会以为入口该在却不在)。折成闭集成员则是替 server 编原因,同样不许。
202
+ * · **非串的值 / accessor 位** ⇒ `{ unknown: '' }` —— 仍然是拒绝,只是连记号都带不出来。
203
+ *
204
+ * ## 🔴 四处不可信读取只认自有数据描述符
205
+ * `caps.workflows` / `caps.workflowsGate` / `gate.engineCan` / `gate.denial` 四处都走
206
+ * {@link ownRead}:accessor / 只挂在原型上的东西一律不执行(`catch` 接得住「抛」,接不住「不返回」)。
207
+ *
208
+ * @param caps 任意 `/v1/capabilities` 回体(只读上面两键;非对象 / `null` ⇒ `undefined`)。
209
+ */
210
+ export declare function projectWorkflowsGate(caps: unknown): WorkflowsGateProjection | undefined;