@sema-agent/client-core 0.62.2 → 0.63.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,91 @@
1
+ /**
2
+ * src/engineNoticeCodes.ts — `engine_notice` 的**码册**与 **audience 表**(0.63.0;L-167;core 7.9.x)。
3
+ *
4
+ * -- 「渲不渲」的判据不在这一端 --------------------------------------------------------------
5
+ * 一条引擎通告该不该占用户的注意力,正确判据**不是**「这一端的渲染表有没有那一格」(那会随各端的
6
+ * 施工进度晃),而是**上游有没有把这个码铸进它的成文码册**:
7
+ * · 码册**内**的码 = 引擎作为治理事实铸出来的,即使这一端还没有专属文案也该让人看见(通用行);
8
+ * · 码册**外**的码 = 转发进同一个水槽的宿主/适配器噪声,呈现面 fail-closed(只落 debug)。
9
+ * audience 回答另一问:这条事实**说给谁听**。一条 operator 行推给终端用户是噪音;一条 user 行只落
10
+ * operator 日志,就是把该告诉他的事瞒下了。两值而已(`user` / `operator`)—— 上游刻意不设第三值:
11
+ * 「这条通告能不能路由到某个会话」不是 audience,而是 `sessionId` 在不在场(**逐次发射**的事实)。
12
+ *
13
+ * -- 为什么是镜像而不是 import ---------------------------------------------------------------
14
+ * `@sema-agent/core` 不是本包消费者的依赖(它既不是 peer 也不是 runtime dep),而本包的 `.d.ts`
15
+ * 一旦引用它,装了本包却没装 core 的下游会当场编译不过。这与本包 `toolResult.ts` / `retryStatus.ts` /
16
+ * `skillsWireCaps.ts` 三处的处置同形:**按 wire 事实镜像,把代价交给门**——
17
+ * `run-engine-notice-catalog-test.mjs` 对**实装 devDep core** 的两份产物逐码逐行双向对账,
18
+ * core 一动这里就先红。上游自己也是这么设计的:它导出码册的理由逐字写着「a downstream table with a
19
+ * row missing … is a mechanically detectable disagreement rather than an argument」。
20
+ *
21
+ * -- 提货记账(码册加员)-----------------------------------------------------------------------
22
+ * · **core 7.10.0 +2**:`delegation.ask_unresolvable`(audience **user** —— 委派链上那只 ask 判不出
23
+ * 归属、没有人可问,这件事的收件人是发起这次委派的**用户**)/ `config.read_face_swapped`
24
+ * (audience **operator** —— READ 容纳面被换过档,是部署事实)。四十八码 ⇒ **五十码**。
25
+ *
26
+ * -- 🔴 单铸律:本模块**不抄**任何一句 core 的措辞 -------------------------------------------
27
+ * `mcp.injection_dropped` 上游立了 `settlement.single_mint` 契约:**宿主只供事实**(哪个会话 /
28
+ * 哪个 server 名 / 四个原因里的哪一个 / 可选哪个字段坏),**core composes** 码字、audience 行与
29
+ * 句子。第二份措辞就是第二个会漂的源。⇒ 本模块只给码册、audience 与**事实窄读器**
30
+ * ({@link readMcpInjectionDrop});转录直接用引擎给的 `message`。门里有一条反向钉:core 那四句
31
+ * 出现在本包源码或产物里 => 当场红。
32
+ */
33
+ /** 一条通告是**说给谁听**的。两值,刻意不设第三值(见模块顶注)。 */
34
+ export type NoticeAudience = 'user' | 'operator';
35
+ /**
36
+ * 引擎**成文码册**的逐字镜像(core `ENGINE_NOTICE_CODES`,**7.10.0 = 五十码**;顺序同源)。
37
+ * 🔴 这是一份**抄件**,不是本包的意见 —— 改它必须同 commit 附 core 坐标,且门会先红。
38
+ * ⚠️ `EngineNotice.code` 的型面在上游**故意留 `string`**(宿主把自己的通告转发进同一个水槽是
39
+ * 被支持的形),所以本表是**判据**不是型 —— 别拿它去窄化那个字段。
40
+
41
+ * 🔴 形制:`Object.freeze` 的数组,**不是**只在类型面只读的 `readonly T[]` —— 后者一行 `.splice()`
42
+ * 就能改,而公面消费者拿到的正是这个实例(本仓已定谳的病形,同 `RESUME_RETRY_LATER_CODES`)。
43
+ */
44
+ export declare const ENGINE_NOTICE_CODES: readonly string[];
45
+ /**
46
+ * 这个码在不在上游码册里。
47
+ * 🔴 **认原始值**:呈现层的消毒器会把一整类字符换成别的字符,拿清洗值来查表等于让
48
+ * `memory\nsession_polluted` 这种脏码冒充册内码。消毒只进文案,不进判据。
49
+ */
50
+ export declare function engineNoticeInCatalog(code: unknown): boolean;
51
+ /**
52
+ * 逐码的 audience 行(core `NOTICE_AUDIENCE_TABLE` 逐行镜像;与 {@link ENGINE_NOTICE_CODES}
53
+ * **逐键 lockstep** —— 上游用编译期 `satisfies` 钉住这一点,本包用门钉住)。
54
+ */
55
+ export declare const ENGINE_NOTICE_AUDIENCE: Readonly<Record<string, NoticeAudience>>;
56
+ /**
57
+ * 这个码是说给谁听的。**表外码一律保守判 `operator`** —— 把一个读不懂的码推给终端用户是最坏的
58
+ * 猜法(他既看不懂也无从下手),而漏给运维看一条只是让他晚一点知道。
59
+ */
60
+ export declare function noticeAudienceOf(code: unknown): NoticeAudience;
61
+ /**
62
+ * `mcp.injection_dropped` 的四个掉落原因(core `MCP_INJECTION_DROP_REASONS` 逐词镜像)。
63
+ * 消费端要自己的措辞时**键在这个词上**,绝不对 `message` 做等值匹配(那是给人读的散文)。
64
+
65
+ * 🔴 形制:`Object.freeze` 的数组,**不是**只在类型面只读的 `readonly T[]` —— 后者一行 `.splice()`
66
+ * 就能改,而公面消费者拿到的正是这个实例(本仓已定谳的病形,同 `RESUME_RETRY_LATER_CODES`)。
67
+ */
68
+ export declare const MCP_INJECTION_DROP_REASONS: readonly string[];
69
+ /** `mcp.injection_dropped` 通告 `detail` 上的事实(宿主在掉落点知道的那几件)。 */
70
+ export interface McpInjectionDropFactsView {
71
+ /** 这条通告投给哪个会话。**必填** —— user audience 的通告没有会话就无处投递。 */
72
+ sessionId: string;
73
+ /** 用户自己写的那个 server 名(引擎的 message 里逐字引了它)。 */
74
+ server: string;
75
+ /** 四个原因之一。 */
76
+ reason: string;
77
+ /** 只有 `malformed_entry` 带:宿主发现哪个键坏了,用户据此知道该改哪儿。 */
78
+ field?: string;
79
+ }
80
+ /**
81
+ * `mcp.injection_dropped` 通告 → 事实;不成形 => `undefined`,绝不抛出。
82
+ *
83
+ * 🔴 **只认自己那一个码**:一条别的通告即使 `detail` 长得像,也不给读(读器按码分家,才不会在
84
+ * 上游给另一个码加同名键的那天悄悄改判)。
85
+ * 🔴 `sessionId` / `server` / `reason` 三格缺一 => 整只缺席。core 在**铸点**对同样三格直接抛
86
+ * `TypeError`(「一条没有会话的 user 通告投不到任何地方」);本包在**读点**判缺席而不抛 ——
87
+ * 读点抛只会把一条读不懂的通告升级成一次崩。
88
+ * 🔴 `reason` **按闭集读**:它是消费端分支的键,读不懂的词不许被当成一个能拿去分支的原因。
89
+ * (要渲一句话时,用引擎给的 `message`,见模块顶注的单铸律。)
90
+ */
91
+ export declare function readMcpInjectionDrop(notice: unknown): McpInjectionDropFactsView | undefined;
@@ -0,0 +1,215 @@
1
+ /**
2
+ * src/engineNoticeCodes.ts — `engine_notice` 的**码册**与 **audience 表**(0.63.0;L-167;core 7.9.x)。
3
+ *
4
+ * -- 「渲不渲」的判据不在这一端 --------------------------------------------------------------
5
+ * 一条引擎通告该不该占用户的注意力,正确判据**不是**「这一端的渲染表有没有那一格」(那会随各端的
6
+ * 施工进度晃),而是**上游有没有把这个码铸进它的成文码册**:
7
+ * · 码册**内**的码 = 引擎作为治理事实铸出来的,即使这一端还没有专属文案也该让人看见(通用行);
8
+ * · 码册**外**的码 = 转发进同一个水槽的宿主/适配器噪声,呈现面 fail-closed(只落 debug)。
9
+ * audience 回答另一问:这条事实**说给谁听**。一条 operator 行推给终端用户是噪音;一条 user 行只落
10
+ * operator 日志,就是把该告诉他的事瞒下了。两值而已(`user` / `operator`)—— 上游刻意不设第三值:
11
+ * 「这条通告能不能路由到某个会话」不是 audience,而是 `sessionId` 在不在场(**逐次发射**的事实)。
12
+ *
13
+ * -- 为什么是镜像而不是 import ---------------------------------------------------------------
14
+ * `@sema-agent/core` 不是本包消费者的依赖(它既不是 peer 也不是 runtime dep),而本包的 `.d.ts`
15
+ * 一旦引用它,装了本包却没装 core 的下游会当场编译不过。这与本包 `toolResult.ts` / `retryStatus.ts` /
16
+ * `skillsWireCaps.ts` 三处的处置同形:**按 wire 事实镜像,把代价交给门**——
17
+ * `run-engine-notice-catalog-test.mjs` 对**实装 devDep core** 的两份产物逐码逐行双向对账,
18
+ * core 一动这里就先红。上游自己也是这么设计的:它导出码册的理由逐字写着「a downstream table with a
19
+ * row missing … is a mechanically detectable disagreement rather than an argument」。
20
+ *
21
+ * -- 提货记账(码册加员)-----------------------------------------------------------------------
22
+ * · **core 7.10.0 +2**:`delegation.ask_unresolvable`(audience **user** —— 委派链上那只 ask 判不出
23
+ * 归属、没有人可问,这件事的收件人是发起这次委派的**用户**)/ `config.read_face_swapped`
24
+ * (audience **operator** —— READ 容纳面被换过档,是部署事实)。四十八码 ⇒ **五十码**。
25
+ *
26
+ * -- 🔴 单铸律:本模块**不抄**任何一句 core 的措辞 -------------------------------------------
27
+ * `mcp.injection_dropped` 上游立了 `settlement.single_mint` 契约:**宿主只供事实**(哪个会话 /
28
+ * 哪个 server 名 / 四个原因里的哪一个 / 可选哪个字段坏),**core composes** 码字、audience 行与
29
+ * 句子。第二份措辞就是第二个会漂的源。⇒ 本模块只给码册、audience 与**事实窄读器**
30
+ * ({@link readMcpInjectionDrop});转录直接用引擎给的 `message`。门里有一条反向钉:core 那四句
31
+ * 出现在本包源码或产物里 => 当场红。
32
+ */
33
+ /**
34
+ * 引擎**成文码册**的逐字镜像(core `ENGINE_NOTICE_CODES`,**7.10.0 = 五十码**;顺序同源)。
35
+ * 🔴 这是一份**抄件**,不是本包的意见 —— 改它必须同 commit 附 core 坐标,且门会先红。
36
+ * ⚠️ `EngineNotice.code` 的型面在上游**故意留 `string`**(宿主把自己的通告转发进同一个水槽是
37
+ * 被支持的形),所以本表是**判据**不是型 —— 别拿它去窄化那个字段。
38
+
39
+ * 🔴 形制:`Object.freeze` 的数组,**不是**只在类型面只读的 `readonly T[]` —— 后者一行 `.splice()`
40
+ * 就能改,而公面消费者拿到的正是这个实例(本仓已定谳的病形,同 `RESUME_RETRY_LATER_CODES`)。
41
+ */
42
+ export const ENGINE_NOTICE_CODES = Object.freeze([
43
+ 'config.autocompact_window_clamped',
44
+ 'config.env_timeout_discarded',
45
+ 'config.materialize_env_discarded',
46
+ 'config.models_swapped',
47
+ 'config.read_face_swapped',
48
+ 'config.read_face_deployment_clamped',
49
+ 'config.tool_model_gate_removed',
50
+ 'config.tool_model_gate_unknown_class',
51
+ 'config.tool_model_gate_env_invalid',
52
+ 'config.tool_card_undeclared',
53
+ 'config.tool_face_undeclared',
54
+ 'config.tool_face_invalid',
55
+ 'config.durable_gate_unavailable',
56
+ 'config.peer_admission_out_of_range',
57
+ 'config.peer_lane_unmounted',
58
+ 'peer.inbound_disposition',
59
+ 'peer.held_settled',
60
+ 'peer.idle_subscription',
61
+ 'classifier.denial_limit',
62
+ 'checkpoint.execution_outcome_unrecorded',
63
+ 'delegation.transcript_integrity',
64
+ 'delegation.ask_unresolvable',
65
+ 'mcp.injection_dropped',
66
+ 'mcp.revocation_probe_failed',
67
+ 'workflow.governance_key_stripped',
68
+ 'workflow.agent_option_ignored',
69
+ 'memory.session_polluted',
70
+ 'memory.harvest_quarantined',
71
+ 'memory.delegation_static_mark_waived',
72
+ 'memory.content_class_declared',
73
+ 'memory.hold_opened',
74
+ 'memory.hold_released',
75
+ 'memory.hold_disposed',
76
+ 'memory.consolidation_recommended',
77
+ 'memory.consolidation_committed',
78
+ 'memory.consolidation_conflict',
79
+ 'memory.consolidation_incomplete',
80
+ 'memory.consolidation_refused',
81
+ 'memory.consolidation_withheld',
82
+ 'route.fallback_to_primary',
83
+ 'route.base_url_changed_key_unchanged',
84
+ 'task.user_steer_undrained',
85
+ 'task.user_followup_undrained',
86
+ 'steering.parked_input_blocked',
87
+ 'task.turn_interrupted',
88
+ 'task.halt_unconsumed',
89
+ 'task.late_approval',
90
+ 'memory.capture_opted_out',
91
+ 'memory.capture_optout_unpersisted',
92
+ 'tool_result.offload_put_failed',
93
+ ]);
94
+ const CATALOG = new Set(ENGINE_NOTICE_CODES);
95
+ /**
96
+ * 这个码在不在上游码册里。
97
+ * 🔴 **认原始值**:呈现层的消毒器会把一整类字符换成别的字符,拿清洗值来查表等于让
98
+ * `memory\nsession_polluted` 这种脏码冒充册内码。消毒只进文案,不进判据。
99
+ */
100
+ export function engineNoticeInCatalog(code) {
101
+ return typeof code === 'string' && CATALOG.has(code);
102
+ }
103
+ /**
104
+ * 逐码的 audience 行(core `NOTICE_AUDIENCE_TABLE` 逐行镜像;与 {@link ENGINE_NOTICE_CODES}
105
+ * **逐键 lockstep** —— 上游用编译期 `satisfies` 钉住这一点,本包用门钉住)。
106
+ */
107
+ export const ENGINE_NOTICE_AUDIENCE = Object.freeze({
108
+ 'config.autocompact_window_clamped': 'operator',
109
+ 'config.env_timeout_discarded': 'operator',
110
+ 'config.materialize_env_discarded': 'operator',
111
+ 'config.models_swapped': 'operator',
112
+ 'config.read_face_swapped': 'operator',
113
+ 'config.read_face_deployment_clamped': 'operator',
114
+ 'config.tool_model_gate_removed': 'operator',
115
+ 'config.tool_model_gate_unknown_class': 'operator',
116
+ 'config.tool_model_gate_env_invalid': 'operator',
117
+ 'config.tool_card_undeclared': 'operator',
118
+ 'config.tool_face_undeclared': 'operator',
119
+ 'config.tool_face_invalid': 'operator',
120
+ 'config.durable_gate_unavailable': 'user',
121
+ 'config.peer_admission_out_of_range': 'operator',
122
+ 'config.peer_lane_unmounted': 'operator',
123
+ 'peer.inbound_disposition': 'user',
124
+ 'peer.held_settled': 'user',
125
+ 'peer.idle_subscription': 'user',
126
+ 'classifier.denial_limit': 'user',
127
+ 'checkpoint.execution_outcome_unrecorded': 'operator',
128
+ 'delegation.transcript_integrity': 'operator',
129
+ 'delegation.ask_unresolvable': 'user',
130
+ 'mcp.injection_dropped': 'user',
131
+ 'mcp.revocation_probe_failed': 'operator',
132
+ 'workflow.governance_key_stripped': 'operator',
133
+ 'workflow.agent_option_ignored': 'operator',
134
+ 'memory.session_polluted': 'user',
135
+ 'memory.harvest_quarantined': 'user',
136
+ 'memory.delegation_static_mark_waived': 'user',
137
+ 'memory.content_class_declared': 'operator',
138
+ 'memory.hold_opened': 'user',
139
+ 'memory.hold_released': 'user',
140
+ 'memory.hold_disposed': 'user',
141
+ 'memory.consolidation_recommended': 'operator',
142
+ 'memory.consolidation_committed': 'operator',
143
+ 'memory.consolidation_conflict': 'operator',
144
+ 'memory.consolidation_incomplete': 'operator',
145
+ 'memory.consolidation_refused': 'operator',
146
+ 'memory.consolidation_withheld': 'user',
147
+ 'route.fallback_to_primary': 'operator',
148
+ 'route.base_url_changed_key_unchanged': 'operator',
149
+ 'task.user_steer_undrained': 'user',
150
+ 'task.user_followup_undrained': 'user',
151
+ 'steering.parked_input_blocked': 'user',
152
+ 'task.turn_interrupted': 'user',
153
+ 'task.halt_unconsumed': 'user',
154
+ 'task.late_approval': 'user',
155
+ 'memory.capture_opted_out': 'user',
156
+ 'memory.capture_optout_unpersisted': 'user',
157
+ 'tool_result.offload_put_failed': 'operator',
158
+ });
159
+ /**
160
+ * 这个码是说给谁听的。**表外码一律保守判 `operator`** —— 把一个读不懂的码推给终端用户是最坏的
161
+ * 猜法(他既看不懂也无从下手),而漏给运维看一条只是让他晚一点知道。
162
+ */
163
+ export function noticeAudienceOf(code) {
164
+ // 🔴 **自有属性判据**(异源对抗复审 [medium]):`Object.freeze` 冻的是自有属性,原型链原样还在 ⇒
165
+ // 一个来自 wire 的 `constructor` / `toString` / `__proto__` 会命中 `Object.prototype` 上的成员,
166
+ // 本口于是交出一个**函数** —— 它既不是 `user` 也不是 `operator`,按两值分发的宿主两条分支都进不去
167
+ // (保守缺省也跟着被绕过)。这是本仓已定谳的病形:wire 键控的表一律按自有属性查。
168
+ const row = typeof code === 'string' && Object.hasOwn(ENGINE_NOTICE_AUDIENCE, code)
169
+ ? ENGINE_NOTICE_AUDIENCE[code]
170
+ : undefined;
171
+ return row ?? 'operator';
172
+ }
173
+ /**
174
+ * `mcp.injection_dropped` 的四个掉落原因(core `MCP_INJECTION_DROP_REASONS` 逐词镜像)。
175
+ * 消费端要自己的措辞时**键在这个词上**,绝不对 `message` 做等值匹配(那是给人读的散文)。
176
+
177
+ * 🔴 形制:`Object.freeze` 的数组,**不是**只在类型面只读的 `readonly T[]` —— 后者一行 `.splice()`
178
+ * 就能改,而公面消费者拿到的正是这个实例(本仓已定谳的病形,同 `RESUME_RETRY_LATER_CODES`)。
179
+ */
180
+ export const MCP_INJECTION_DROP_REASONS = Object.freeze([
181
+ 'malformed_entry',
182
+ 'name_reserved_by_deployment',
183
+ 'gate_closed',
184
+ 'over_cap',
185
+ ]);
186
+ const DROP_REASONS = new Set(MCP_INJECTION_DROP_REASONS);
187
+ /**
188
+ * `mcp.injection_dropped` 通告 → 事实;不成形 => `undefined`,绝不抛出。
189
+ *
190
+ * 🔴 **只认自己那一个码**:一条别的通告即使 `detail` 长得像,也不给读(读器按码分家,才不会在
191
+ * 上游给另一个码加同名键的那天悄悄改判)。
192
+ * 🔴 `sessionId` / `server` / `reason` 三格缺一 => 整只缺席。core 在**铸点**对同样三格直接抛
193
+ * `TypeError`(「一条没有会话的 user 通告投不到任何地方」);本包在**读点**判缺席而不抛 ——
194
+ * 读点抛只会把一条读不懂的通告升级成一次崩。
195
+ * 🔴 `reason` **按闭集读**:它是消费端分支的键,读不懂的词不许被当成一个能拿去分支的原因。
196
+ * (要渲一句话时,用引擎给的 `message`,见模块顶注的单铸律。)
197
+ */
198
+ export function readMcpInjectionDrop(notice) {
199
+ if (typeof notice !== 'object' || notice === null || Array.isArray(notice))
200
+ return undefined;
201
+ const n = notice;
202
+ if (n.code !== 'mcp.injection_dropped')
203
+ return undefined;
204
+ const d = n.detail;
205
+ if (typeof d !== 'object' || d === null || Array.isArray(d))
206
+ return undefined;
207
+ const o = d;
208
+ const sessionId = typeof o.sessionId === 'string' && o.sessionId.length > 0 ? o.sessionId : undefined;
209
+ const server = typeof o.server === 'string' && o.server.length > 0 ? o.server : undefined;
210
+ const reason = typeof o.reason === 'string' && DROP_REASONS.has(o.reason) ? o.reason : undefined;
211
+ if (sessionId === undefined || server === undefined || reason === undefined)
212
+ return undefined;
213
+ const field = typeof o.field === 'string' && o.field.length > 0 ? o.field : undefined;
214
+ return { sessionId, server, reason, ...(field !== undefined ? { field } : {}) };
215
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * src/gateVocabulary.ts — 门词汇两张表的**唯一措辞铸点**:`DeniedBy`(谁拒的,九词)与
3
+ * `AskOrigin`(谁问的,十一词)(0.63.0;sdk 8.8.0 / core 7.9.0 / L-162 ③)。
4
+ *
5
+ * ── 为什么措辞归包 ─────────────────────────────────────────────────────────────────────────
6
+ * 这两个词到用户面前时都要变成一句人话。让三端各写一份,同一次拒在 TUI / web / desktop 上会
7
+ * 是三句不同的话,而其中至少两句迟早会落后于上游加词。表与句子收在这里,加词只有一处要改,
8
+ * 而门会在上游加词的当天先红。
9
+ *
10
+ * ── 🔴 两张表的**开闭各按其出处**,兜底句因此必须分家 ──────────────────────────────────────
11
+ * 这不是风格问题,是两条不同的事实:
12
+ * · **`DeniedBy` 在这条 wire 上是真闭集**。引擎的 `screenGateOutcome` 把「`deniedBy` 出闭集」
13
+ * 列为记录**缺陷**,而 server 对有缺陷的门记录是**整条不上帧**(report + withhold)⇒ 一个
14
+ * 词表外的 `deniedBy` **结构上到不了消费端**,它到达的形式是 `gate` **整键缺席**。所以本包
15
+ * 真读到一个表外词时,唯一诚实的话是「这条门记录本不该长这样」——那是记录缺陷的信号,
16
+ * 不是「有个新词」。sdk 的型面因此也没有 `(string & {})` 逃生口。
17
+ * · **`AskOrigin` 是真开集**。`tool_approval` 帧上的 `origin` 在 server 侧**只判非空串、不判
18
+ * 成员** ⇒ core 加一个新词的当天,一个**合法**的帧就会带着表外值到达消费端。所以表外词的
19
+ * 那一句必须说「这个词比这一端新」,把它渲成坏记录会让用户去查一个根本不存在的故障。
20
+ * ⇒ 两句兜底逐字不同,门里有一条钉守着它们不被合并成一句。
21
+ *
22
+ * ── 词表是**抄件**不是意见 ────────────────────────────────────────────────────────────────
23
+ * 两张表逐词逐序抄自 sdk 的联合声明,门 `run-gate-vocabulary-test.mjs` 从实装 `.d.ts` 里解出成员
24
+ * 与本表**双向等值**对账(缺一词红、多一词红、顺序不同也红)。改它必须同 commit 附上游坐标。
25
+ */
26
+ import type { DeniedBy } from '@sema-agent/sdk';
27
+ /**
28
+ * **谁拒的** —— 判决是这次 deny 的那一**层**(sdk `DeniedBy` / core `DENIED_BY_VALUES`,
29
+ * 7.6.0 起八词、**7.9.0 起九词**)。顺序逐字同源。
30
+ * 🔴 真闭集(见模块顶注):表外词到不了消费端,所以这张表**没有**逃生口。
31
+
32
+ * 🔴 形制:`Object.freeze` 的数组,**不是**只在类型面只读的 `readonly T[]` —— 后者一行 `.splice()`
33
+ * 就能改,而公面消费者拿到的正是这个实例(本仓已定谳的病形,同 `RESUME_RETRY_LATER_CODES`)。
34
+ */
35
+ export declare const GATE_DENIED_BY_WORDS: readonly DeniedBy[];
36
+ /**
37
+ * 一个 `deniedBy` 词 → 一句人话。**唯一铸点**(三端共用;端零自拼)。
38
+ *
39
+ * 🔴 表外词的那一句说的是「**这条门记录本不该长这样**」,不是「有个新词」——理由见模块顶注
40
+ * (出集的记录在 server 侧整条不上帧,所以真读到一个表外词意味着记录有缺陷)。原样带上那个
41
+ * 词:运维要拿它去问上游。
42
+ * 🔴 非串 / 空串同样走兜底 —— 一个读不出的层名不是「没有层」(deny 臂在类型上就必须点名一层)。
43
+ * 🔴 **查表用 `Object.hasOwn`,不用 `in` / 裸下标**(异源对抗复审 [medium]):`Object.freeze` 冻的是
44
+ * **自有属性**,原型链原样还在 ⇒ 一个来自 wire 的 `constructor` / `toString` / `__proto__` 会命中
45
+ * `Object.prototype` 上的成员,措辞口于是返回一个**函数**而不是一句话:呈现调用当场坏掉,而且
46
+ * 它绕过了上下两条精心分家的兜底。这是本仓已定谳的病形(wire 键控的表一律用 Map 或自有属性判据)。
47
+ */
48
+ export declare function gateDeniedByDetail(deniedBy: unknown): string;
49
+ /**
50
+ * 一只 ask 的**出身** —— 谁问的(sdk `AskOrigin` / core `ASK_ORIGINS`;7.5.x 及以前八词、
51
+ * 7.6.0 十词、**7.9.0 起十一词**)。顺序逐字同源。
52
+ * 🔴 真开集(见模块顶注):server 只判非空串,新词会带着合法的帧到达。
53
+
54
+ * 🔴 形制:`Object.freeze` 的数组,**不是**只在类型面只读的 `readonly T[]` —— 后者一行 `.splice()`
55
+ * 就能改,而公面消费者拿到的正是这个实例(本仓已定谳的病形,同 `RESUME_RETRY_LATER_CODES`)。
56
+ */
57
+ export declare const ASK_ORIGIN_WORDS: readonly string[];
58
+ /**
59
+ * 一个 `origin` 词 → 一句人话。**唯一铸点**(三端共用;端零自拼)。
60
+ *
61
+ * 🔴 表外词的那一句说的是「**这个词比这一端新**」,不是「坏记录」——理由见模块顶注(server 只判
62
+ * 非空串,core 加词当天合法的帧就带着它到达)。原样带上那个词,并明说这次仍然是在问人:
63
+ * 读不懂出身**不改变**这只 ask 要人回答这件事。
64
+ * 🔴 非串 / 空串:同走兜底但词位渲 `(none)` —— 「没报出身」与「报了一个读不懂的出身」在这一句里
65
+ * 不必分家(两者对用户的下一步相同:照常回答这只 ask),但都**不许**被折成十一词里的任何一个。
66
+ */
67
+ export declare function askOriginDetail(origin: unknown): string;
@@ -0,0 +1,134 @@
1
+ /**
2
+ * **谁拒的** —— 判决是这次 deny 的那一**层**(sdk `DeniedBy` / core `DENIED_BY_VALUES`,
3
+ * 7.6.0 起八词、**7.9.0 起九词**)。顺序逐字同源。
4
+ * 🔴 真闭集(见模块顶注):表外词到不了消费端,所以这张表**没有**逃生口。
5
+
6
+ * 🔴 形制:`Object.freeze` 的数组,**不是**只在类型面只读的 `readonly T[]` —— 后者一行 `.splice()`
7
+ * 就能改,而公面消费者拿到的正是这个实例(本仓已定谳的病形,同 `RESUME_RETRY_LATER_CODES`)。
8
+ */
9
+ export const GATE_DENIED_BY_WORDS = Object.freeze([
10
+ 'policy',
11
+ 'hook',
12
+ 'org',
13
+ 'persisted_rule',
14
+ 'classifier',
15
+ 'plan_mode',
16
+ 'compliance',
17
+ 'write_protection',
18
+ 'ask_resolution',
19
+ ]);
20
+ /**
21
+ * 逐词一句人话。**九句刻意逐字互异**:对用户/运维是九条不同的下一步(改部署策略 / 改 hook /
22
+ * 找组织管理员 / 撤自己的常驻规则 / 调 auto 模式 / 退出 plan 模式 / 找合规 / 改写保护表 /
23
+ * 看那次审批的结算)。
24
+ */
25
+ const DENIED_BY_SENTENCES = Object.freeze({
26
+ /** 部署 ToolPolicy 拒(直接拒,或复查一次已批准的编辑),或审批-编辑链撞了轮次上限。 */
27
+ policy: "denied by this deployment's permission policy",
28
+ /** PreToolUse hook 拒 / 抛 / 从未作答 —— 「没作决定的 hook」的 fail-closed 拦阻也按 hook 层归因。 */
29
+ hook: 'denied by a PreToolUse hook',
30
+ /** 组织策略规则拒。 */
31
+ org: 'denied by an organization rule',
32
+ /** **这个人自己**名下的一条常驻 deny 规则拒(他 settings 的 deny 表导进来的那类行)。
33
+ * 它是 `org` 的**个人店同胞**:同属「可以否掉一次人已经给过的批准」的复查层。 */
34
+ persisted_rule: 'denied by a persisted rule',
35
+ /** auto 模式分类器**直接**拒(它的 ASK 侧角色是 `origin: "denial_limit_fallback"`,不是这一格)。 */
36
+ classifier: 'denied by the auto-mode classifier',
37
+ /** plan 模式对写工具的只读拦阻。 */
38
+ plan_mode: 'denied because plan mode only allows reads',
39
+ /** 合规的调用期锁。 */
40
+ compliance: 'denied by a compliance lock',
41
+ /** 一次被批准的编辑被限制链改写到了**没有任何审批覆盖**的写保护路径上。 */
42
+ write_protection: 'denied because the write landed on a write-protected path',
43
+ /** 这次 ask 的**结算本身**就是拒(有人说了不 / 窗到期 / 没人可问……)—— 细节在 `settlement`。 */
44
+ ask_resolution: 'denied when the approval was resolved',
45
+ });
46
+ /**
47
+ * 一个 `deniedBy` 词 → 一句人话。**唯一铸点**(三端共用;端零自拼)。
48
+ *
49
+ * 🔴 表外词的那一句说的是「**这条门记录本不该长这样**」,不是「有个新词」——理由见模块顶注
50
+ * (出集的记录在 server 侧整条不上帧,所以真读到一个表外词意味着记录有缺陷)。原样带上那个
51
+ * 词:运维要拿它去问上游。
52
+ * 🔴 非串 / 空串同样走兜底 —— 一个读不出的层名不是「没有层」(deny 臂在类型上就必须点名一层)。
53
+ * 🔴 **查表用 `Object.hasOwn`,不用 `in` / 裸下标**(异源对抗复审 [medium]):`Object.freeze` 冻的是
54
+ * **自有属性**,原型链原样还在 ⇒ 一个来自 wire 的 `constructor` / `toString` / `__proto__` 会命中
55
+ * `Object.prototype` 上的成员,措辞口于是返回一个**函数**而不是一句话:呈现调用当场坏掉,而且
56
+ * 它绕过了上下两条精心分家的兜底。这是本仓已定谳的病形(wire 键控的表一律用 Map 或自有属性判据)。
57
+ */
58
+ export function gateDeniedByDetail(deniedBy) {
59
+ if (typeof deniedBy === 'string' && Object.hasOwn(DENIED_BY_SENTENCES, deniedBy)) {
60
+ return DENIED_BY_SENTENCES[deniedBy];
61
+ }
62
+ const word = typeof deniedBy === 'string' && deniedBy.length > 0 ? deniedBy : '(none)';
63
+ return `denied, but the layer name ${word} is not one this gate record should be able to carry — engines withhold records that carry an unknown layer, so this one is damaged`;
64
+ }
65
+ /**
66
+ * 一只 ask 的**出身** —— 谁问的(sdk `AskOrigin` / core `ASK_ORIGINS`;7.5.x 及以前八词、
67
+ * 7.6.0 十词、**7.9.0 起十一词**)。顺序逐字同源。
68
+ * 🔴 真开集(见模块顶注):server 只判非空串,新词会带着合法的帧到达。
69
+
70
+ * 🔴 形制:`Object.freeze` 的数组,**不是**只在类型面只读的 `readonly T[]` —— 后者一行 `.splice()`
71
+ * 就能改,而公面消费者拿到的正是这个实例(本仓已定谳的病形,同 `RESUME_RETRY_LATER_CODES`)。
72
+ */
73
+ export const ASK_ORIGIN_WORDS = Object.freeze([
74
+ 'content_question',
75
+ 'unresolvable',
76
+ 'org_unavailable',
77
+ 'org_rule',
78
+ 'rule_store_unavailable',
79
+ 'hook',
80
+ 'ask_rule',
81
+ 'denial_limit_fallback',
82
+ 'shell_gate_tighten',
83
+ 'safety_tighten',
84
+ 'policy',
85
+ ]);
86
+ /**
87
+ * 逐词一句人话。**十一句刻意逐字互异** —— 尤其是这三对刻意分家的同胞:
88
+ * · `org_unavailable` / `rule_store_unavailable`:两个「治理源这次读不出来 ⇒ fail-closed 问人」,
89
+ * 一个是**组织**店、一个是**这个人自己**的持久规则店,下一步找的人不同;
90
+ * · `shell_gate_tighten` / `safety_tighten`:两条 tighten 分成两个词,正是为了说出**哪一层**
91
+ * 引擎逻辑提的问(粗粒度 shellGate 教条 vs 调用的显式事实:egress 标 / 不可逆标 / 写保护);
92
+ * · `org_rule` / `ask_rule`:组织的规则 vs 这个人自己的常驻 ask 行。
93
+ */
94
+ const ASK_ORIGIN_SENTENCES = Object.freeze({
95
+ content_question: 'the tool itself asked you a question',
96
+ unresolvable: 'the gate could not decide on its own, so it asks',
97
+ org_unavailable: 'the organization policy was unavailable, so this call asks',
98
+ org_rule: 'an organization rule asks about this call',
99
+ rule_store_unavailable: 'the rule store was unavailable, so this call asks',
100
+ hook: 'a PreToolUse hook asked about this call',
101
+ ask_rule: 'a persisted ask rule matches this call',
102
+ denial_limit_fallback: 'the auto-mode classifier hit its denial limit and handed this call back to you',
103
+ shell_gate_tighten: 'this deployment asks about every shell command at this gate setting',
104
+ safety_tighten: 'the gate tightened on this call’s own facts (network egress, irreversibility, or a protected write)',
105
+ policy: 'this deployment’s permission policy asks about this call',
106
+ });
107
+ /**
108
+ * 一个 `origin` 词 → 一句人话。**唯一铸点**(三端共用;端零自拼)。
109
+ *
110
+ * 🔴 表外词的那一句说的是「**这个词比这一端新**」,不是「坏记录」——理由见模块顶注(server 只判
111
+ * 非空串,core 加词当天合法的帧就带着它到达)。原样带上那个词,并明说这次仍然是在问人:
112
+ * 读不懂出身**不改变**这只 ask 要人回答这件事。
113
+ * 🔴 非串 / 空串:同走兜底但词位渲 `(none)` —— 「没报出身」与「报了一个读不懂的出身」在这一句里
114
+ * 不必分家(两者对用户的下一步相同:照常回答这只 ask),但都**不许**被折成十一词里的任何一个。
115
+ */
116
+ export function askOriginDetail(origin) {
117
+ // 🔴 自有属性判据,理由与 `gateDeniedByDetail` 逐字相同(冻结不移除原型)。
118
+ const known = typeof origin === 'string' && Object.hasOwn(ASK_ORIGIN_SENTENCES, origin)
119
+ ? ASK_ORIGIN_SENTENCES[origin]
120
+ : undefined;
121
+ if (known !== undefined)
122
+ return known;
123
+ const word = typeof origin === 'string' && origin.length > 0 ? origin : '(none)';
124
+ return `this call asks; its origin ${word} is a word newer than this client, so answer it as usual`;
125
+ }
126
+ /**
127
+ * **编译期对账钉**(不出公面):本包两张表的成员必须真属于 sdk 的两个联合。
128
+ * 🔴 `DeniedBy` 那一张钉得**双向**(它在 wire 上是真闭集,型面也闭);`AskOrigin` 那一张只钉
129
+ * 单向(它有逃生口,任何字符串都合法 —— 钉反向等于把开集当闭集用)。
130
+ */
131
+ const _deniedByWordsPin = GATE_DENIED_BY_WORDS;
132
+ void _deniedByWordsPin;
133
+ const _askOriginWordsPin = ASK_ORIGIN_WORDS;
134
+ void _askOriginWordsPin;
@@ -27,11 +27,22 @@
27
27
  * 七类共用一格,wire 上分不出来是设计)。唯一可分的是 503 `state.rule_import_retry`:那一支意味着
28
28
  * **票还在**,原样重试即可(与「这张票没了」是相反的处置)。
29
29
  *
30
+ * ── 🔴 三态规则身份:`behavior` 必填且不给默认(server ≥7.67.0 / sdk 8.8.0 BREAKING)──────────
31
+ * 规则车道此前只有 allow,撤销体按 `(rule, scope)` 两元组寻址就够了。7.67.0 起 deny/ask 也进同一个
32
+ * 店 —— **同一段文本的 `deny` 与 `allow` 是两条不同的行**。一次瞄准 `allow` 的撤销若被默认成同文本
33
+ * 的 `deny`,删掉的是一条本该留着的**拒绝**规则,而调用方收到的是一个 200(引擎侧
34
+ * `sameRuleIdentity` 顶注逐字写着这一形)。⇒ 本模块两条纪律:
35
+ * · 读:{@link persistedRuleBehaviorOf} 读不出这一格就答 `undefined`,**绝不补 allow 默认**;
36
+ * 列举半场**照样把那一行交出去**(丢行 = 把一条活规则从治理清单里藏起来,正是
37
+ * {@link listAllPersistedRules} 拒绝做的事),呈现走 {@link persistedRuleBehaviorLabel};
38
+ * · 写:{@link revokeTargetFromPersistedRule} 读不出这一格就**拒铸撤销体** —— 猜一态的代价是
39
+ * 不可逆地删掉另一态的行,而拒铸的代价只是治理面上少一个按钮。
40
+ *
30
41
  * ── UNTRUSTED ───────────────────────────────────────────────────────────────────────────────
31
42
  * 规则文本与 `skipped[].reason` 都是引擎/用户 settings 侧的内容,只渲染绝不当代码用;`reason` 是
32
43
  * **给人看的散文不是机读码**(SDK 头注),分类只许按第一个 `:` 前缀,并且要容得下**没有前缀**的形。
33
44
  */
34
- import type { CcImportLayer, CcImportPrepareResult, CcImportRedeemResult, PersistedRule, RuleListParams, RuleListResult, RuleRevokeRequest, RuleRevokeResult } from '@sema-agent/sdk';
45
+ import type { CcImportLayer, CcImportPrepareResult, CcImportRedeemResult, PersistedRule, RuleBehavior, RuleListParams, RuleListResult, RuleRevokeRequest, RuleRevokeResult } from '@sema-agent/sdk';
35
46
  /** 规则车道消费的 `client.rules` 切片(注入缝:宿主给真 SDK facade,测试给假件)。 */
36
47
  export interface RulesFacade {
37
48
  list(params?: RuleListParams, opts?: {
@@ -121,6 +132,57 @@ export type RulesFailure =
121
132
  message: string;
122
133
  };
123
134
  export declare function classifyRulesFailure(e: unknown): RulesFailure;
135
+ /**
136
+ * 一条持久规则是哪一态 —— sdk `RuleBehavior` 的**运行期镜像**(引擎侧同一张闭三词表)。
137
+ * `allow` = 一次人工批准的常驻形(卡上点的「不再询问」);`deny` / `ask` = 「永不运行」与
138
+ * 「每次都问我」的常驻形。**一套文法、一种规范拼法、一族匹配器**服务三态,态是**文本旁边的
139
+ * 一格**,永远不是文本的一部分。
140
+ * 🔴 门 `run-rules-side-test.mjs` G9a 对 sdk 的联合成员**逐词等值**对账(缺一词红、多一词红)。
141
+
142
+ * 🔴 形制:`Object.freeze` 的数组,**不是**只在类型面只读的 `readonly T[]` —— 后者一行 `.splice()`
143
+ * 就能改,而公面消费者拿到的正是这个实例(本仓已定谳的病形,同 `RESUME_RETRY_LATER_CODES`)。
144
+ */
145
+ export declare const PERSISTED_RULE_BEHAVIORS: readonly RuleBehavior[];
146
+ /**
147
+ * 这一行是哪一态。**闭三词**之外(含整键缺席的老 worker 行)一律 `undefined`。
148
+ *
149
+ * 🔴 **绝不补默认**:缺席是「这台 worker 说不出来」,不是 `allow`。补一个默认会让呈现面
150
+ * 对一条 deny 行说 allow,而撤销面据此瞄准另一态。
151
+ * 🔴 这里**按闭集读**(与本包其余开集读口刻意不同):这一格是**身份**的一格,不是一个展示词——
152
+ * 读不懂的身份必须判「读不出」,而不是原样当成一个能拿去撤销的态。呈现层要原样渲那个词,
153
+ * 走 {@link persistedRuleBehaviorLabel}(它是开集的)。
154
+ */
155
+ export declare function persistedRuleBehaviorOf(rule: unknown): RuleBehavior | undefined;
156
+ /** 读不出态时呈现面渲的那个词。**逐字锚**(三端共用一句;端零自拼)。 */
157
+ export declare const PERSISTED_RULE_BEHAVIOR_UNKNOWN = "(unknown)";
158
+ /**
159
+ * 治理行上「态」那一列的**唯一措辞铸点**。
160
+ * 🔴 三态渲词本身;**表外词原样渲**(开集 —— 引擎加词那天治理面照样看得见那个词,不至于把一条
161
+ * 真实的行渲成「读不出」);**整键缺席/坏形**才渲 {@link PERSISTED_RULE_BEHAVIOR_UNKNOWN} ——
162
+ * 它不冒充三态里的任何一个,读到它的人知道下一步是升级那台 worker。
163
+ */
164
+ export declare function persistedRuleBehaviorLabel(rule: unknown): string;
165
+ /**
166
+ * 一条列举行 → 撤销体(`rules.revoke()` 的入参)。**按内容寻址**,这条面上下没有任何 id。
167
+ *
168
+ * 🔴 **身份三元组逐字回传**(`behavior` / `rule` / `scope`):拼法等价但不逐字相同的 scope 什么都
169
+ * 匹配不上,而且会得到一个笑呵呵的 `no-op`;所以原样送回引擎给的那串字节,别拿解析后的形重铸。
170
+ * 🔴 **只带身份三键 + 可选 `principal`**:`source` / `status` 是**派生键**,回传它们会被拒。
171
+ * 🔴 **`behavior` 读不出 ⇒ 拒铸(返回 `undefined`)**,这正是本次 BREAKING 的全部理由:猜一态的
172
+ * 代价是不可逆地删掉另一态的行且调用方收到 200;拒铸的代价只是治理面少一个按钮。调用方据此
173
+ * 藏掉撤销入口(并可用 {@link persistedRuleBehaviorLabel} 告诉用户为什么)。
174
+ * 🔴 `rule` / `scope` 任一读不出也拒铸:按 `undefined` 撤销什么都对不上。
175
+ * 🔴 **显式给了一个无效 `principal` ≠ 没给**(异源对抗复审 [high]):`principal` **缺席**明确表示
176
+ * 「撤**我自己的**」。若把一个显式传进来的空串/非串静默降级成缺席,一次瞄准**别人**的 operator
177
+ * 撤销就会转向**调用者自己**的规则桶,并且拿到一个 200 —— 而目标值算空在治理面上是常见形
178
+ * (表单空字段、一次没查到的用户名)。⇒ 键**在场但值不合法**时**拒铸整只**,与 `behavior`
179
+ * 读不出同一条处置。`opts` 整只不给、或给了但不含这个键,才是「缺席 = 撤我自己的」那个合法意图。
180
+ * ⚠️ 合法值**原样送出不 trim**:principal 的规范形归引擎,壳替它 trim 会让两端对「同一个人」的
181
+ * 拼法各有一份判据。
182
+ */
183
+ export declare function revokeTargetFromPersistedRule(rule: unknown, opts?: {
184
+ principal?: string;
185
+ }): RuleRevokeRequest | undefined;
124
186
  /** 一次「列全」的结果。`rules` 恒是**完整**清单(翻不完 ⇒ 走 failure,绝不交半份清单)。 */
125
187
  export type ListAllPersistedRulesOutcome = {
126
188
  ok: true;