@sema-agent/client-core 0.27.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/hitl/hitlBridge.d.ts +12 -0
- package/dist/hitl/hitlBridge.js +43 -3
- package/dist/hitl/toolApprovalWire.d.ts +73 -3
- package/dist/hitl/toolApprovalWire.js +52 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
|
|
|
23
23
|
|
|
24
24
|
## Scope
|
|
25
25
|
|
|
26
|
-
**Version:** 0.
|
|
26
|
+
**Version:** 0.28.0
|
|
27
27
|
|
|
28
28
|
- **Today** — the adapter seam, the whole `adapt()` pipeline (all 14 A-layer arms plus the
|
|
29
29
|
B/D/E tool-card layers), the notification/caps/model families, the adapter kernel (stream driver
|
|
@@ -216,7 +216,7 @@ public-surface guard checks that last one).
|
|
|
216
216
|
| `scripts/run-client-core-portability-test.mjs` | Kernel / A-layer / index import closures, the runtime-dependency equality gate, barrel reachability, and a real esbuild `--platform=browser` bundle |
|
|
217
217
|
| `scripts/run-client-core-diff-test.mjs` | Differential equivalence against the CLI reference bridge + replay-id invariant + ledger round-trip |
|
|
218
218
|
| `scripts/run-seat-contract-keys-test.mjs` | The seat IPC contract: verb list ↔ SPEC ↔ types, element-wise |
|
|
219
|
-
| `scripts/run-approval-frame-keys-test.mjs` | The tool-approval frame key mirror, element-wise against the SDK's runtime anchor |
|
|
219
|
+
| `scripts/run-approval-frame-keys-test.mjs` | The tool-approval frame key mirror, element-wise against the SDK's runtime anchor (one carve-out: AHEAD_OF_ANCHOR entries — keys the server already emits but the SDK anchor has not caught up to — may lead by one generation; the gate turns red the day the SDK catches up, forcing the entry's removal) |
|
|
220
220
|
| `scripts/run-print-bash-iserror-test.mjs` | The print lane's Bash `is_error` authority (structured over regex) |
|
|
221
221
|
| `scripts/run-sdk-floor-test.mjs` | The SDK version floor — and, more to the point, that the *installed* type declarations still carry the keys this package reads |
|
|
222
222
|
| `scripts/run-engine-vocab-floor-test.mjs` | Engine-mirrored vocabularies (structured card whitelist, self-reported tool face, control verbs, recogniser sets) against the *installed* `@sema-agent/core` |
|
|
@@ -66,6 +66,18 @@
|
|
|
66
66
|
* backend supplies the SIGNAL; the shell owns the chrome.
|
|
67
67
|
*/
|
|
68
68
|
import type { AgentEvent, ApprovalDecision, PendingCheckpoint, CheckpointGate, PlanReviewRequest, AssistantTaskStatus } from '@sema-agent/sdk';
|
|
69
|
+
/** durable `/decide` 腿的既有缺省拒因(不带归因时逐字不变 —— 0.27.0 及之前的 wire 字节)。 */
|
|
70
|
+
export declare const DEFAULT_DENY_REASON = "The user rejected this tool use";
|
|
71
|
+
/** server 两条腿共用的 reason 字符上限(超限 413,决断被打回)。 */
|
|
72
|
+
export declare const MAX_DENY_REASON_CHARS = 4096;
|
|
73
|
+
/**
|
|
74
|
+
* 归因原文 → 可上 wire 的形。缺席/非串/纯空白 ⇒ `undefined`(调用方自定回落:deny 腿落
|
|
75
|
+
* {@link DEFAULT_DENY_REASON},plan-review 腿键不落);超上限 ⇒ 截到上限并 debug 留痕。
|
|
76
|
+
* 🔴 截断**边界安全**:切点落在代理对中间会产出孤高代理 —— server 长度检查放行,但 UTF-8
|
|
77
|
+
* 落库/入签时末尾退化成 U+FFFD,两侧字节不一致还会毒化签名对账。末码元是高代理就再退一位。
|
|
78
|
+
* 🔴 只做归因,不参与裁决:任何返回值都不改变这次决断本身。
|
|
79
|
+
*/
|
|
80
|
+
export declare function denyReasonForWire(reason: string | undefined, tag: string): string | undefined;
|
|
69
81
|
/** CC `PermissionDecision` 的结构切片 —— 本桥运行时真正读的三个键。 */
|
|
70
82
|
export interface HitlPermissionDecisionLike {
|
|
71
83
|
behavior: 'allow' | 'ask' | 'deny';
|
package/dist/hitl/hitlBridge.js
CHANGED
|
@@ -1,4 +1,38 @@
|
|
|
1
1
|
import { eventSeq } from '../adapter/types.js';
|
|
2
|
+
import { hostLog } from '../host.js';
|
|
3
|
+
// ── deny/plan-review 归因的 wire 窄化(0.28.0 发版扫描 F1/F2/F3 收编;单源,三条腿共用)──────
|
|
4
|
+
//
|
|
5
|
+
// server 对 `/decide` 与 plan-review 两条腿的 `reason` 同限 `MAX_APPROVAL_REASON_CHARS`(4096 字符,
|
|
6
|
+
// 超限 **413 `reason_too_large`**;server 侧 reason 入签后无法替发送方截 —— 超长原样发出去,丢掉的
|
|
7
|
+
// 不是归因而是**整次决断**:413 ⇒ 决断没送达 ⇒ run 留 suspended)。所以三条 reason 出口(卡 deny /
|
|
8
|
+
// canUseTool deny / plan-review)全部经本窄化器,绝不裸发。
|
|
9
|
+
/** durable `/decide` 腿的既有缺省拒因(不带归因时逐字不变 —— 0.27.0 及之前的 wire 字节)。 */
|
|
10
|
+
export const DEFAULT_DENY_REASON = 'The user rejected this tool use';
|
|
11
|
+
/** server 两条腿共用的 reason 字符上限(超限 413,决断被打回)。 */
|
|
12
|
+
export const MAX_DENY_REASON_CHARS = 4096;
|
|
13
|
+
/**
|
|
14
|
+
* 归因原文 → 可上 wire 的形。缺席/非串/纯空白 ⇒ `undefined`(调用方自定回落:deny 腿落
|
|
15
|
+
* {@link DEFAULT_DENY_REASON},plan-review 腿键不落);超上限 ⇒ 截到上限并 debug 留痕。
|
|
16
|
+
* 🔴 截断**边界安全**:切点落在代理对中间会产出孤高代理 —— server 长度检查放行,但 UTF-8
|
|
17
|
+
* 落库/入签时末尾退化成 U+FFFD,两侧字节不一致还会毒化签名对账。末码元是高代理就再退一位。
|
|
18
|
+
* 🔴 只做归因,不参与裁决:任何返回值都不改变这次决断本身。
|
|
19
|
+
*/
|
|
20
|
+
export function denyReasonForWire(reason, tag) {
|
|
21
|
+
if (typeof reason !== 'string')
|
|
22
|
+
return undefined;
|
|
23
|
+
const trimmed = reason.trim();
|
|
24
|
+
if (trimmed === '')
|
|
25
|
+
return undefined;
|
|
26
|
+
if (trimmed.length <= MAX_DENY_REASON_CHARS)
|
|
27
|
+
return trimmed;
|
|
28
|
+
let cut = trimmed.slice(0, MAX_DENY_REASON_CHARS);
|
|
29
|
+
const last = cut.charCodeAt(cut.length - 1);
|
|
30
|
+
if (last >= 0xd800 && last <= 0xdbff)
|
|
31
|
+
cut = cut.slice(0, -1); // 孤高代理:退一位,绝不发半个字符
|
|
32
|
+
hostLog('debug', `hitlBridge: reason for ${tag} truncated ${trimmed.length}→${cut.length} chars ` +
|
|
33
|
+
`(server caps approval reason at ${MAX_DENY_REASON_CHARS}; over-cap is a 413 reason_too_large that would drop the DECISION, not just the reason)`);
|
|
34
|
+
return cut;
|
|
35
|
+
}
|
|
2
36
|
// ── Errors the bridge surfaces (fail-closed; never auto-retried) ───────────────
|
|
3
37
|
/** A safety stop: a contract law was about to be violated (e.g. a binding mismatch, a wrong-gate route).
|
|
4
38
|
* The caller MUST re-present to the human or surface the error — NEVER silently retry or auto-decide
|
|
@@ -254,8 +288,12 @@ export class HitlBridge {
|
|
|
254
288
|
// (PlanReviewRequest 是 SDK 外部 wire 型,不能改;诚实缺席 = 键不落,不传 `editedPlan: undefined`)。
|
|
255
289
|
if (outcome.decision === 'edit' && outcome.editedPlan !== undefined)
|
|
256
290
|
req.editedPlan = outcome.editedPlan;
|
|
257
|
-
|
|
258
|
-
|
|
291
|
+
// 0.28.0 发版扫描 F2:reason 经窄化器(超 4096 截断防 413 丢整次计划决断);空白 ⇒ 键不落。
|
|
292
|
+
if (outcome.reason !== undefined) {
|
|
293
|
+
const r = denyReasonForWire(outcome.reason, `plan-review ${this.taskId}`);
|
|
294
|
+
if (r !== undefined)
|
|
295
|
+
req.reason = r;
|
|
296
|
+
}
|
|
259
297
|
return this.client.assistant.planReview(this.taskId, req, opts);
|
|
260
298
|
}
|
|
261
299
|
// ── §5. resource_limit: continue ────────────────────────────────────────────
|
|
@@ -305,7 +343,9 @@ export function makeHitlCanUseTool(bridge, prompt) {
|
|
|
305
343
|
(await prompt({ toolName: tool.name, input, toolUseID, gate }));
|
|
306
344
|
if (decision.behavior === 'deny') {
|
|
307
345
|
// Deny → cancel-by-deny (contract/04 §2.4). The deny message rides `reason`.
|
|
308
|
-
|
|
346
|
+
// 0.28.0 发版扫描 F1(P2):message 逐字嵌原始命令(壳侧 bashPermissions 无上限)——必须与
|
|
347
|
+
// 卡腿同门经窄化器截到 4096,否则 server 413 丢的是整次 deny(run 留 suspended)。
|
|
348
|
+
await bridge.decideTool({ decision: 'deny', reason: denyReasonForWire(decision.message, `canUseTool ${toolUseID}`) ?? DEFAULT_DENY_REASON }, toolUseID);
|
|
309
349
|
return decision;
|
|
310
350
|
}
|
|
311
351
|
if (decision.behavior === 'allow') {
|
|
@@ -60,6 +60,19 @@
|
|
|
60
60
|
* 🔴 fail-soft 铁律(与 ask 桥同款):leader 队列未注册(print/非 REPL)、pending 蒸发、decide 409/404
|
|
61
61
|
* 都返回 failed —— 调用方回退「flush + 原样终帧」的诚实红,绝不更糟。
|
|
62
62
|
* 🔴 UNTRUSTED:gate args 为模型作文(service 已 redact),只渲染绝不回喂;决断只带 decision 枚举。
|
|
63
|
+
*
|
|
64
|
+
* ── #144 半场(client-core 0.28.0)────────────────────────────────────────────────────────────
|
|
65
|
+
* ① **透传** `persistedRuleShadowed`(server ≥7.13.0,来源锚 = engine fixture
|
|
66
|
+
* `@sema-agent/server/dist/tool-approval.d.ts` 同名键;ADDITIVE、`tool_approval` only)——
|
|
67
|
+
* 帧 → {@link ApprovalCardRequest.persistedRuleShadowed},壳据此渲「你的规则仍在,只是这次被
|
|
68
|
+
* 要求逐次确认」。🔴 它是 **display 值,不是身份通道**:脱敏后的展示文本,不保证与规则店里那条
|
|
69
|
+
* 规则逐字节相同、更不是它的 id;下游「删除/撤销规则」面拿它当身份用 = 删错或删不掉。
|
|
70
|
+
* ② **deny 归因通道**({@link ApprovalCardDenyDecision.reason}):两腿不同形 ——
|
|
71
|
+
* durable `/decide` **有** reason 位 ⇒ 原文上 wire(包内按 server 4096 上限截断);
|
|
72
|
+
* live 帧腿 `/v1/tool-approvals/:id/respond` 是三词闭集、**无** reason 位 ⇒ 只 debug 留痕、不发、
|
|
73
|
+
* 不自造侧信道。⏳ **记账:respond 端点的 reason 位候 server**(server 辖区,到货后本包侧
|
|
74
|
+
* 只需在 opts 上加一位,窄化器 `denyReasonForWire` 已在)。
|
|
75
|
+
* 🔴 方向纪律:reason 只做归因,绝不参与裁决;缺席 ⇒ 现状字节不变。
|
|
63
76
|
*/
|
|
64
77
|
import { type HitlClientLike } from './hitlBridge.js';
|
|
65
78
|
import type { RuleSuggestion, ToolApprovalRespondAck } from '@sema-agent/sdk';
|
|
@@ -111,9 +124,29 @@ export interface ApprovalCardAllowDecision {
|
|
|
111
124
|
* 缺席/空串 = 本次不兑付。表外文本会在编排层被丢键留痕(server 亦拒 rule_not_offered)。 */
|
|
112
125
|
persistRule?: string;
|
|
113
126
|
}
|
|
114
|
-
|
|
127
|
+
/**
|
|
128
|
+
* deny 决断臂(命名形,typeshape B4 口径;0.28.0 因 `reason` 位抽名,与 0.26.0
|
|
129
|
+
* {@link ApprovalCardAllowDecision} 抽名同姿势)。
|
|
130
|
+
*/
|
|
131
|
+
export interface ApprovalCardDenyDecision {
|
|
115
132
|
kind: 'deny';
|
|
116
|
-
|
|
133
|
+
/**
|
|
134
|
+
* #144 半场(0.28.0):这次拒绝的**归因原文**——人手打的拒绝说明,或壳侧权限规则自动 deny 的
|
|
135
|
+
* 机器判据(rule-deny / dontAsk / headless fail-closed 三条路)。缺席/空白 = 不带归因,调用方
|
|
136
|
+
* 落回既有缺省拒因,**现状字节不变**。
|
|
137
|
+
*
|
|
138
|
+
* 🔴 **方向纪律**:reason 只进归因通道,**绝不参与裁决** —— 带不带它、内容是什么,都不改变
|
|
139
|
+
* 这次决断仍是 deny(两条腿上的 `decision` 位逐字节不动)。
|
|
140
|
+
* 🔴 **两腿不同形**(见 {@link surfaceFsApprovalAndDecide} / {@link surfaceToolApprovalFrameAndRespond}):
|
|
141
|
+
* · durable park 腿(`/decide`)wire **有** reason 位 ⇒ 原文上 wire(包内按 server 上限截断);
|
|
142
|
+
* · live 帧腿(`/v1/tool-approvals/:id/respond`)wire 是三词闭集、**无** reason 位 ⇒ 只留痕不发,
|
|
143
|
+
* 绝不自造侧信道(候 server 给 respond 补位)。
|
|
144
|
+
* 与 `{kind:'failed'}` 的 `reason` **不同义**:那一位是「卡面本身不可用」的诊断,不是人的拒因,
|
|
145
|
+
* 两者绝不互相顶替。
|
|
146
|
+
*/
|
|
147
|
+
reason?: string;
|
|
148
|
+
}
|
|
149
|
+
export type ApprovalCardDecision = ApprovalCardAllowDecision | ApprovalCardDenyDecision | {
|
|
117
150
|
kind: 'aborted';
|
|
118
151
|
} | {
|
|
119
152
|
kind: 'failed';
|
|
@@ -150,6 +183,19 @@ export interface ApprovalCardRequest {
|
|
|
150
183
|
* 出身裁剪(MANDATED ask 不提供本档)候 core 5.25.0 出身键 wire 过境,见 #144/[3438]/[3442]。
|
|
151
184
|
*/
|
|
152
185
|
ruleSuggestions?: RuleSuggestion[];
|
|
186
|
+
/**
|
|
187
|
+
* 被越级的持久 allow 规则**原文**(#144,原样来自 {@link ToolApprovalFrame.persistedRuleShadowed}
|
|
188
|
+
* 的合形值)——壳据此渲「你的规则仍在,只是这次调用被要求逐次确认」;缺席 = 卡形与 0.27.0
|
|
189
|
+
* 字节不变(不渲该行)。
|
|
190
|
+
*
|
|
191
|
+
* 🔴 **display 值,不是身份通道**:它是一段**已过脱敏**的展示文本(core `inlineUntrusted` +
|
|
192
|
+
* server `redactSecrets`),不保证与规则店里存着的那条规则逐字节相同,更不是规则的 id/句柄。
|
|
193
|
+
* 下游的「删除/撤销这条规则」面**不许**把它当身份用(拿它去 match 规则店 = 删错规则或删不掉),
|
|
194
|
+
* 也不许把它回喂给模型/工具入参;要操作规则必须走规则店自己的枚举与标识。
|
|
195
|
+
* 🔴 **缺席 ≠「你没有规则」**:键只在「有规则命中 ∧ 规则清不掉这只 ask」时在场,绝大多数 ask
|
|
196
|
+
* 压根没有规则命中(缺席),而命中且清掉了的那些根本不会变成卡。
|
|
197
|
+
*/
|
|
198
|
+
persistedRuleShadowed?: string;
|
|
153
199
|
}
|
|
154
200
|
/**
|
|
155
201
|
* 🔴 **拆缝口** —— 弹「三选卡」并等人的决断。壳 = vendored CC `PermissionRequest`;
|
|
@@ -239,6 +285,30 @@ export interface ToolApprovalFrame {
|
|
|
239
285
|
* (`respond` 体 `persistRule.rule`),报表外文本 = server 拒 `rule_not_offered`。
|
|
240
286
|
*/
|
|
241
287
|
ruleSuggestions?: RuleSuggestion[];
|
|
288
|
+
/**
|
|
289
|
+
* server ≥7.13.0(#144 / core 5.25.0,[3438] 接力契约 / [3443] 主件;**ADDITIVE**,
|
|
290
|
+
* `"tool_approval"` only。来源锚 = engine fixture `@sema-agent/server/dist/tool-approval.d.ts`
|
|
291
|
+
* 的同名键)——这只 ask **命中了**调用方的一条持久 allow 规则,而那条规则**没能清掉它**。
|
|
292
|
+
* 值 = 被越级的那条规则**原文**(core `AskRequest.persistedRuleShadowed`,core 侧已过
|
|
293
|
+
* `inlineUntrusted`;server 侧与 `message`/`sourceAgentName` 同待遇 `redactSecrets` 后上帧)。
|
|
294
|
+
*
|
|
295
|
+
* 🔴 它存在的理由是一次真实的用户面回归:core 5.25.0 收窄消音边界后(「Allow rules silence the
|
|
296
|
+
* CLASSIFIER's questions, never a MANDATED one」),此前被规则消掉的 ask 重新出现 —— 没有这个键,
|
|
297
|
+
* 人看到的是「我明明点过『不再询问』,它怎么又问」,唯一合理的结论是「我的规则坏了/没存上」。
|
|
298
|
+
*
|
|
299
|
+
* 🔴 **缺席 ≠「你没有规则」**:键只在「有规则命中 ∧ 规则清不掉这只 ask」时在场。绝大多数 ask
|
|
300
|
+
* 压根没有规则命中(缺席),而**命中且清掉了**的那些根本不会变成 ask(被消音了,没有卡)。
|
|
301
|
+
* 🔴 与 {@link governanceForced} **刻意分列不合并**:那个键答「门是谁下的」(运维治理层),本键
|
|
302
|
+
* 答「你那条规则怎么了」。不可消音的来源有三类(governance / doctrine `always` / 工具自带 mark),
|
|
303
|
+
* 合并会让后两类要么谎报治理出身、要么丢掉规则解释。
|
|
304
|
+
* 🔴 **UNTRUSTED-for-display,且不是身份通道** —— 详见
|
|
305
|
+
* {@link ApprovalCardRequest.persistedRuleShadowed} 的同名警示(下游删除规则面不许拿它当规则身份)。
|
|
306
|
+
*
|
|
307
|
+
* 耐久路(park 行)的对偶是 `gate.riskDescriptor.shadowedRule`(server 对 riskDescriptor 整体透传);
|
|
308
|
+
* 本包的 durable 腿今天不 stamp 任何展示键(见 {@link surfaceFsApprovalAndDecide} 的卡入参),
|
|
309
|
+
* 那一路的消费是独立的一件,不在本键的施工面内。
|
|
310
|
+
*/
|
|
311
|
+
persistedRuleShadowed?: string;
|
|
242
312
|
}
|
|
243
313
|
/** {@link ToolApprovalFrame.delegation} 的形(命名形,不用内联匿名 —— typeshape 门 B4 棘轮口径)。 */
|
|
244
314
|
export interface ToolApprovalDelegation {
|
|
@@ -254,7 +324,7 @@ export interface ToolApprovalDelegation {
|
|
|
254
324
|
* `TOOL_APPROVAL_FRAME_KEYS` 比对——SDK additive 增键时对账当天红,不再人肉追平。
|
|
255
325
|
* 下面两个类型钉保证镜像与 interface 本身不可能漂移(少键/多键都是编译错)。
|
|
256
326
|
*/
|
|
257
|
-
export declare const TOOL_APPROVAL_FRAME_KEYS_MIRROR: readonly ["type", "approvalId", "toolCallId", "toolName", "sourceTaskId", "fromSubagent", "sourceAgentName", "message", "args", "argsOmitted", "governanceForced", "ruleSuggestions", "delegation", "outcome"];
|
|
327
|
+
export declare const TOOL_APPROVAL_FRAME_KEYS_MIRROR: readonly ["type", "approvalId", "toolCallId", "toolName", "sourceTaskId", "fromSubagent", "sourceAgentName", "message", "args", "argsOmitted", "governanceForced", "ruleSuggestions", "persistedRuleShadowed", "delegation", "outcome"];
|
|
258
328
|
/** 子代帧判别:显式键 fromSubagent(core 1.378 RB-39②)优先;缺席退 sourceTaskId 在场性权宜式
|
|
259
329
|
* (server 1.258 [1549]①3,旧代际兼容)。 */
|
|
260
330
|
export declare function isFromSubagent(frame: ToolApprovalFrame): boolean;
|
|
@@ -60,8 +60,21 @@
|
|
|
60
60
|
* 🔴 fail-soft 铁律(与 ask 桥同款):leader 队列未注册(print/非 REPL)、pending 蒸发、decide 409/404
|
|
61
61
|
* 都返回 failed —— 调用方回退「flush + 原样终帧」的诚实红,绝不更糟。
|
|
62
62
|
* 🔴 UNTRUSTED:gate args 为模型作文(service 已 redact),只渲染绝不回喂;决断只带 decision 枚举。
|
|
63
|
+
*
|
|
64
|
+
* ── #144 半场(client-core 0.28.0)────────────────────────────────────────────────────────────
|
|
65
|
+
* ① **透传** `persistedRuleShadowed`(server ≥7.13.0,来源锚 = engine fixture
|
|
66
|
+
* `@sema-agent/server/dist/tool-approval.d.ts` 同名键;ADDITIVE、`tool_approval` only)——
|
|
67
|
+
* 帧 → {@link ApprovalCardRequest.persistedRuleShadowed},壳据此渲「你的规则仍在,只是这次被
|
|
68
|
+
* 要求逐次确认」。🔴 它是 **display 值,不是身份通道**:脱敏后的展示文本,不保证与规则店里那条
|
|
69
|
+
* 规则逐字节相同、更不是它的 id;下游「删除/撤销规则」面拿它当身份用 = 删错或删不掉。
|
|
70
|
+
* ② **deny 归因通道**({@link ApprovalCardDenyDecision.reason}):两腿不同形 ——
|
|
71
|
+
* durable `/decide` **有** reason 位 ⇒ 原文上 wire(包内按 server 4096 上限截断);
|
|
72
|
+
* live 帧腿 `/v1/tool-approvals/:id/respond` 是三词闭集、**无** reason 位 ⇒ 只 debug 留痕、不发、
|
|
73
|
+
* 不自造侧信道。⏳ **记账:respond 端点的 reason 位候 server**(server 辖区,到货后本包侧
|
|
74
|
+
* 只需在 opts 上加一位,窄化器 `denyReasonForWire` 已在)。
|
|
75
|
+
* 🔴 方向纪律:reason 只做归因,绝不参与裁决;缺席 ⇒ 现状字节不变。
|
|
63
76
|
*/
|
|
64
|
-
import { HitlBridge, HitlSafetyError, findPendingForTask } from './hitlBridge.js';
|
|
77
|
+
import { DEFAULT_DENY_REASON, HitlBridge, HitlSafetyError, denyReasonForWire, findPendingForTask, } from './hitlBridge.js';
|
|
65
78
|
import { hostLog } from '../host.js';
|
|
66
79
|
import { createSessionSlot, DEFAULT_SESSION_KEY } from '../sessionSlot.js';
|
|
67
80
|
import { readEngineActiveBgTasks } from '../fleet/fleetLedger.js';
|
|
@@ -195,6 +208,8 @@ function cardPortMissReason() {
|
|
|
195
208
|
}
|
|
196
209
|
return 'no approval card port installed (print/non-REPL mode, or host forgot installApprovalCardPort)';
|
|
197
210
|
}
|
|
211
|
+
// deny 归因窄化(0.28.0 发版扫描 F1 收编):单源下沉 hitlBridge —— 三条 reason 出口(卡 deny /
|
|
212
|
+
// canUseTool deny / plan-review)共用一把,坐标与理由见 hitlBridge.ts 的导出处顶注。
|
|
198
213
|
/**
|
|
199
214
|
* 弹 CC 三选卡 → 等决断 → 经 HitlBridge decide(durable park 腿,server 桥前的 checkpointGate 面;
|
|
200
215
|
* 合成单测/旧引擎兜底保留)。decide 成功即引擎驱到下一状态(sync);调用方随后 attach runs.events
|
|
@@ -277,7 +292,11 @@ export async function surfaceFsApprovalAndDecide(deps, taskId, argsByCall, signa
|
|
|
277
292
|
}
|
|
278
293
|
case 'deny':
|
|
279
294
|
try {
|
|
280
|
-
|
|
295
|
+
// #144 半场(0.28.0):卡给的拒因原样上 wire(`/decide` 本就有 reason 位)—— 壳侧三处权限
|
|
296
|
+
// 规则自动 deny(rule-deny / dontAsk / headless fail-closed)的真实拒因此前只能落 debug 日志,
|
|
297
|
+
// 引擎与运维队列上看到的都是那句无差别的缺省文案。缺席/空白 ⇒ 缺省,现状字节不变。
|
|
298
|
+
// 🔴 归因不改裁决:无论 reason 是什么,这一支恒 `decision:'deny'`。
|
|
299
|
+
await bridge.decideTool({ decision: 'deny', reason: denyReasonForWire(card.reason, `task ${taskId}`) ?? DEFAULT_DENY_REASON }, gatedCallId, signal ? { signal } : undefined, pending);
|
|
281
300
|
return { kind: 'decided', gatedCallId, denied: true };
|
|
282
301
|
}
|
|
283
302
|
catch (e) {
|
|
@@ -309,6 +328,11 @@ export const TOOL_APPROVAL_FRAME_KEYS_MIRROR = [
|
|
|
309
328
|
// sdk 6.14.0 追平(#225 件1,client-core 0.26.0):server 7.12.0 起真发规则候选 —— 门
|
|
310
329
|
// (run-approval-frame-keys-test.mjs)在 bump SDK 当天恰红抓获本键缺席,本次补齐。
|
|
311
330
|
'ruleSuggestions',
|
|
331
|
+
// #144(client-core 0.28.0):server 7.13.0 起真发 `persistedRuleShadowed`(证据=engine fixture
|
|
332
|
+
// 的 `@sema-agent/server/dist/tool-approval.d.ts`)。⚠️ 本键**领先** SDK 运行期锚一代
|
|
333
|
+
// (sdk 6.14.0/6.15.0 的 TOOL_APPROVAL_FRAME_KEYS 都还没有它)⇒ 对账门里有一条**带退出条件**的
|
|
334
|
+
// 领先登记(AHEAD_OF_ANCHOR):SDK 锚一旦补上,那条登记当场红,逼人删登记而不是让豁免长住。
|
|
335
|
+
'persistedRuleShadowed',
|
|
312
336
|
'delegation',
|
|
313
337
|
'outcome',
|
|
314
338
|
];
|
|
@@ -518,11 +542,37 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
|
|
|
518
542
|
: {}),
|
|
519
543
|
// #225 件1:规则候选透传(合形项;缺席/坏形 ⇒ 键不 stamp,卡形与 0.25.0 字节不变)。
|
|
520
544
|
...(ruleSuggestions !== undefined ? { ruleSuggestions } : {}),
|
|
545
|
+
// #144:被越级的持久规则原文(UNTRUSTED-for-display)。窄化=**非空白串才 stamp**,坏形降缺席
|
|
546
|
+
// (server 明说空串不铸键:「空串是坏值不是『空规则』」)—— 一格空白的规则解释比没有解释更坏。
|
|
547
|
+
// 🔴 stamp 的是**原文字节**(不 trim、不截、不改写):展示值一改写就不再是「你那条规则」的原文;
|
|
548
|
+
// 渲染面的长度夹取归壳,与 `message`/`sourceAgentName` 同族口径(本模块对展示文本零加工)。
|
|
549
|
+
...(typeof frame.persistedRuleShadowed === 'string' && frame.persistedRuleShadowed.trim() !== ''
|
|
550
|
+
? { persistedRuleShadowed: frame.persistedRuleShadowed }
|
|
551
|
+
: {}),
|
|
521
552
|
});
|
|
522
553
|
const decision = card.kind === 'allow' ? (card.allowSession ? 'allow_session' : 'allow') : 'deny';
|
|
523
554
|
if (card.kind === 'failed') {
|
|
524
555
|
hostLog('debug', `liveToolApprovalWire: approval card unavailable (${card.reason}) — fail-closed deny for ${frame.approvalId}`);
|
|
525
556
|
}
|
|
557
|
+
// #144 半场记账(0.28.0):respond 端点(POST /v1/tool-approvals/:id/respond)的体是**三词闭集**
|
|
558
|
+
// (`allow|allow_session|deny`,server `parseToolApprovalResponse`)—— wire 上**没有 reason 位**。
|
|
559
|
+
// 所以这条腿上的拒因只留痕、**不发**:塞进 `updatedInput` 会把归因喂回工具入参面,另开一条消息
|
|
560
|
+
// /header 是自造侧信道,两者都比「暂时没有这个位」更坏。durable `/decide` 腿有位、已透传(上面
|
|
561
|
+
// 那条腿)。⏳ 记账:respond 端点的 reason 位候 server(本包侧零改动即可消费,窄化器已在)。
|
|
562
|
+
// 🔴 留痕只写**元数据**(在场 + 长度),绝不把拒因正文拷进日志(对抗复审命中):
|
|
563
|
+
// 正文是自由文本(人打的拒绝说明 / 规则原文),可能带路径、命令、被拒内容的片段,而 `hostLog`
|
|
564
|
+
// 的落点是宿主任意注入的 sink(文件/远端都可能)—— 把它原样拷过去既是外溢面,也给了
|
|
565
|
+
// 换行/控制字符注入日志行的口子。**而且正文本来就在宿主手里**:这条腿上的 reason 正是宿主
|
|
566
|
+
// 自己的卡口返回的,回喂给它自己的 sink 零诊断增量。要看正文的场景是 durable 腿,那一路它
|
|
567
|
+
// 真的上了 wire。
|
|
568
|
+
// 0.28.0 发版扫描 F4:本腿留痕**不借** denyReasonForWire —— 那个窄化器的超限日志讲的是
|
|
569
|
+
// 「413 会丢掉决断」,对 respond 端点是假话(它根本不收 reason,无 413 可言);且截断后 len
|
|
570
|
+
// 恒 4096,掩盖真实丢弃量。本地判空白+报**原始**长度,只说这条腿真实发生的事:归因整条没发。
|
|
571
|
+
if (card.kind === 'deny' && typeof card.reason === 'string' && card.reason.trim() !== '') {
|
|
572
|
+
hostLog('debug', `liveToolApprovalWire: ${frame.approvalId} card supplied a deny reason (len=${card.reason.length}) but the respond ` +
|
|
573
|
+
'endpoint carries no reason slot (three-word closed set) — attribution not sent on this lane, no side channel invented ' +
|
|
574
|
+
'(content not logged: it is host-authored free text and the host already holds it)');
|
|
575
|
+
}
|
|
526
576
|
try {
|
|
527
577
|
// abort 后的 deny 仍要送达(引擎侧同款 cancel-by-deny)——不带已 aborted 的 signal。
|
|
528
578
|
// #225 件1:卡决断携带的 persistRule 只在「确是帧候选之一的原文」时上 wire —— server 本就
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/client-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "Client-side session runtime shared by every sema human client (TUI / web / desktop): sema wire frames (AgentEvent) -> CC session vocabulary (SDKMessage) with dual-plane output (transcript/chrome), deterministic transcript ids, lane discipline as a type, and the notification/dedup ledgers. Every CC-skin shape is collected here so the wire itself stays neutral. Blackboard [1832] design axioms; [1651]/[1652]/[1653] signed seam design. Renamed from @sema-agent/wire-cc-adapter (0.1.x).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|