@sema-agent/client-core 0.49.0 → 0.51.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,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;
@@ -0,0 +1,255 @@
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 { CAPABILITY_SELF_ORCHESTRATION_REQUIRED } from './engineErrorCodes.js';
53
+ // ── ① 501 拒绝的判型(纯判定)──────────────────────────────────────────────────────────────
54
+ /**
55
+ * server 今天唯一说得出口的拒绝原因:**中心侧的准入解析器不在场**,于是本部署无法为这位
56
+ * principal 判「能不能用 workflow」,fail-closed 拒掉。
57
+ *
58
+ * 🔴 它是**闭集的今日单成员**,不是开集识别表:server 明天加第二个成员时,本包读到的会是
59
+ * {@link SelfOrchestrationDenialReason} 的 `'unknown'` 臂(或 caps 侧的
60
+ * {@link WorkflowsGateUnknownDenial}),**绝不**塌进本成员 —— 塌进去就是替 server 编了一个
61
+ * 它没说过的原因。
62
+ */
63
+ const ENTITLEMENT_RESOLVER_ABSENT = 'entitlement_resolver_absent';
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 const SELF_ORCHESTRATION_RETRY_WITHOUT = Object.freeze(['selfOrchestration', 'settings.ultracode']);
80
+ /**
81
+ * 一次任务提交失败(任意抛出物)→ 「这是不是 selfOrchestration 准入拒绝」。**永不抛**。
82
+ *
83
+ * 🔴 **两条判据是合取,且都不许放宽**:
84
+ * · `status === 501` —— 别的状态码一律 `null`(400 是「键的值不对」、403 是「越权」,
85
+ * 两者都不该靠去掉键来重试);
86
+ * · `errorCode` **恰等** {@link CAPABILITY_SELF_ORCHESTRATION_REQUIRED} —— 不是 `capability.`
87
+ * 前缀判。这个码是**复用码**,与它同前缀的兄弟(别的能力位没接线)去掉这两个键**也不会**变成
88
+ * 可受理,把它们一起拖进重发臂 = 白发一次请求 + 给用户一句错误的原因。
89
+ * · 两者**都**要在场:无码的 501 判不出(如实 `null`,绝不挑一个猜);码对但状态码不是 501
90
+ * 同样 `null`(那不是本闸说的话)。
91
+ *
92
+ * 🔴 **结构视图读,不 `instanceof`**(与 `classifyMemoryStatusFailure` / `classifyRulesFailure`
93
+ * 逐字同因):抛出来的是不是 SDK 的 `APIError` 由**宿主**决定 —— 跨 realm / 双 SDK 实例下
94
+ * `instanceof` 会把一个读得懂的错判成读不懂。SDK 的 `APIError` 形与端自己合成的
95
+ * `{ status, errorCode }` 裸形因此走**同一条**读法。
96
+ *
97
+ * 🔴 **取属性本身要设防**:`e` 是任意抛出物,可以是带抛错 getter 的对象或敌意 `Proxy`,
98
+ * 那样连 `e.status` 这一下都会抛。本函数常常在 `catch` 块里被调用,而 `catch` 内抛出的异常
99
+ * **不会**再被同一个 `try` 接住 —— 分类器一抛,调用方那句「永不抛」当场破功。
100
+ *
101
+ * 🔴 **本函数走普通属性读取(会沿原型、会执行 getter),这是刻意的**,理由与边界见文件头
102
+ * 「getter 零执行只对 `projectWorkflowsGate` 成立」那一段:抛出物可能是类实例,判据位坐在原型上。
103
+ * 能收窄的那一半已经收窄 —— `denial` **只在 501 ∧ 恰码两条判据都通过之后**才读(异源对抗复审
104
+ * [medium] 采纳):一个根本不匹配的错误不该被本函数跑一次它的 getter,而 `status`/`errorCode`
105
+ * 两位是判据本身,没有更早的地方可以躲。
106
+ *
107
+ * @returns 认得 ⇒ 判决(端据此去键重发**一次**,见档 §13);认不得 ⇒ `null`(按普通失败呈现)。
108
+ */
109
+ export function classifySelfOrchestrationRefusal(e) {
110
+ let status;
111
+ let code;
112
+ try {
113
+ const o = e;
114
+ const rawStatus = o?.status;
115
+ const rawCode = o?.errorCode;
116
+ status = typeof rawStatus === 'number' ? rawStatus : undefined;
117
+ code = typeof rawCode === 'string' ? rawCode : undefined;
118
+ }
119
+ catch {
120
+ // 读不动这个抛出物 ⇒ 判不出它是不是本族的拒绝。不抛,不猜。
121
+ return null;
122
+ }
123
+ if (status !== 501)
124
+ return null;
125
+ if (code !== CAPABILITY_SELF_ORCHESTRATION_REQUIRED)
126
+ return null;
127
+ // 🔴 `denial` **只在两条判据都通过之后**才读(见头注):它不是判据,是判决上的一个附加位 ——
128
+ // 无条件先读等于让每一个**根本不匹配**的抛出物都被跑一次它的 getter。这一读同样设防
129
+ // (它可能抛),读不动就当缺席落 `'unknown'`,绝不让本函数向外抛。
130
+ let denial;
131
+ try {
132
+ denial = e?.denial;
133
+ }
134
+ catch {
135
+ denial = undefined;
136
+ }
137
+ return {
138
+ kind: 'self-orchestration-denied',
139
+ retryWithout: SELF_ORCHESTRATION_RETRY_WITHOUT,
140
+ // 闭集成员才认;缺席 / 别的值 / 非串一律 `'unknown'`(见 SelfOrchestrationDenialReason 头注)。
141
+ reason: denial === ENTITLEMENT_RESOLVER_ABSENT ? ENTITLEMENT_RESOLVER_ABSENT : 'unknown',
142
+ };
143
+ }
144
+ // ── ② 去掉两个意图键(纯结构操作)──────────────────────────────────────────────────────────
145
+ /** 自有键判(不查原型:`settings` 是 wire 体上的自有位,原型上的同名东西不是这次要删的那个)。 */
146
+ const hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
147
+ export function stripSelfOrchestrationIntent(req) {
148
+ const out = { ...req };
149
+ delete out.selfOrchestration;
150
+ const settings = out.settings;
151
+ if (typeof settings === 'object' && settings !== null && !Array.isArray(settings)) {
152
+ if (hasOwn(settings, 'ultracode')) {
153
+ const rest = { ...settings };
154
+ delete rest.ultracode;
155
+ if (Object.keys(rest).length === 0)
156
+ delete out.settings;
157
+ else
158
+ out.settings = rest;
159
+ }
160
+ }
161
+ return out;
162
+ }
163
+ const ABSENT = { kind: 'absent' };
164
+ const ACCESSOR = { kind: 'accessor' };
165
+ /**
166
+ * 从一个**不可信**对象上取一位:只认**自有数据描述符**的 `value`。
167
+ *
168
+ * 🔴 为什么不用 `obj[key]`(与 `src/hitl/crashConverged.ts` 的 `ownDataValue` 同一条纪律):
169
+ * 普通属性读取会 ① **执行** accessor,② 一路查到**原型**上去。执行意味着同步跑别人的代码 ——
170
+ * 而 `try/catch` 接得住「抛」,接不住「不返回」:一只忙等 / 死循环的 getter 会把这条读面所在的
171
+ * 线程**永久**钉住。真供给来自 `JSON.parse`,每一位都是自有数据位 ⇒ 这条对真 caps 零影响。
172
+ *
173
+ * 🔴 三态**刻意分开**(不是「读到 / 没读到」两态):`absent` 与 `accessor` 在 `denial` 那一位上
174
+ * 处置相反 —— 键不在场是「闸没说拒绝」,而在场却执行不得是「拒了但读不出」。合成两态就会把
175
+ * 后者折进前者,那正是本模块最不该犯的错。
176
+ *
177
+ * ⚠️ `Object.getOwnPropertyDescriptor` 自己对代理会触发 trap(可能抛)⇒ 抛出时归 `accessor`
178
+ * (「在场但读不出」),不归 `absent`。
179
+ */
180
+ function ownRead(o, key) {
181
+ try {
182
+ const d = Object.getOwnPropertyDescriptor(o, key);
183
+ if (d === undefined)
184
+ return ABSENT;
185
+ if ('get' in d || 'set' in d)
186
+ return ACCESSOR;
187
+ return { kind: 'data', value: d.value };
188
+ }
189
+ catch {
190
+ return ACCESSOR;
191
+ }
192
+ }
193
+ /**
194
+ * `GET /v1/capabilities` 回体 → workflows 闸的**三位投影**。纯函数,**永不抛**。
195
+ *
196
+ * ## 🔴 缺席 vs 关着:两件不同的事,判据不许合流
197
+ * · `caps` 非对象 / `workflows` 位不是布尔(缺席、accessor、类型漂了)⇒ 返回 `undefined` ——
198
+ * 「本部署没告诉我这件事」。端此时**零渲染**:绝不渲「workflow 不可用」之类的话,那是替
199
+ * server 下一个它没说过的断言([honest-absence-not-fabricated-zero])。
200
+ * · `workflows` 是布尔而 `workflowsGate` 缺席(或在场却读不出)⇒
201
+ * `{ workflows, engineCan: undefined, denial: null }` —— 老 server 的真形:合流结论有,
202
+ * 两根轴没有。`engineCan === undefined` 就是端判「这台 server 没有这个闸」的那一位。
203
+ * ⇒ 两档的返回形不同(`undefined` vs 对象),端拿 `=== undefined` 一刀分开。
204
+ *
205
+ * ## 🔴 `denial` 的四档(**未知值绝不折成 `null`**)
206
+ * · 键缺席 / 值是 `null` 或 `undefined` ⇒ `null`(闸明说没有拒绝);
207
+ * · 恰等闭集成员 ⇒ 该字面量(端可以据此说人话);
208
+ * · **别的串** ⇒ `{ unknown: <该串> }` —— server 加了第二个成员而本包还没跟车。折成 `null`
209
+ * 会让端渲出「没有任何拒绝」,而真相是「拒了,只是我不认得原因」:那是这条面上最坏方向的
210
+ * 假断言(用户会以为入口该在却不在)。折成闭集成员则是替 server 编原因,同样不许。
211
+ * · **非串的值 / accessor 位** ⇒ `{ unknown: '' }` —— 仍然是拒绝,只是连记号都带不出来。
212
+ *
213
+ * ## 🔴 四处不可信读取只认自有数据描述符
214
+ * `caps.workflows` / `caps.workflowsGate` / `gate.engineCan` / `gate.denial` 四处都走
215
+ * {@link ownRead}:accessor / 只挂在原型上的东西一律不执行(`catch` 接得住「抛」,接不住「不返回」)。
216
+ *
217
+ * @param caps 任意 `/v1/capabilities` 回体(只读上面两键;非对象 / `null` ⇒ `undefined`)。
218
+ */
219
+ export function projectWorkflowsGate(caps) {
220
+ try {
221
+ if (typeof caps !== 'object' || caps === null)
222
+ return undefined;
223
+ const w = ownRead(caps, 'workflows');
224
+ if (w.kind !== 'data' || typeof w.value !== 'boolean')
225
+ return undefined;
226
+ const workflows = w.value;
227
+ const g = ownRead(caps, 'workflowsGate');
228
+ if (g.kind !== 'data' || typeof g.value !== 'object' || g.value === null || Array.isArray(g.value)) {
229
+ // 闸缺席,或在场却读不出(accessor / 形漂了)——两档都是「这台 server 没给我两根轴」,
230
+ // `engineCan: undefined` 就是端判这一档的那一位;此时 `denial: null` 不是「没拒绝」这个
231
+ // 断言,而是「本档没有拒绝信息」,端按 `engineCan === undefined` 一并读。
232
+ return { workflows, engineCan: undefined, denial: null };
233
+ }
234
+ const gate = g.value;
235
+ const ec = ownRead(gate, 'engineCan');
236
+ const engineCan = ec.kind === 'data' && typeof ec.value === 'boolean' ? ec.value : undefined;
237
+ const d = ownRead(gate, 'denial');
238
+ let denial;
239
+ if (d.kind === 'absent')
240
+ denial = null;
241
+ else if (d.kind === 'accessor')
242
+ denial = { unknown: '' };
243
+ else if (d.value === null || d.value === undefined)
244
+ denial = null;
245
+ else if (d.value === ENTITLEMENT_RESOLVER_ABSENT)
246
+ denial = ENTITLEMENT_RESOLVER_ABSENT;
247
+ else
248
+ denial = { unknown: typeof d.value === 'string' ? d.value : '' };
249
+ return { workflows, engineCan, denial };
250
+ }
251
+ catch {
252
+ // 敌意 / 坏载体:这次供给读不出 ⇒ 诚实缺席,不向外抛。
253
+ return undefined;
254
+ }
255
+ }