@sema-agent/client-core 0.12.2 → 0.14.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 +15 -2
- package/dist/abortableSleep.d.ts +29 -0
- package/dist/abortableSleep.js +43 -0
- package/dist/adapt/arms.d.ts +67 -0
- package/dist/adapt/arms.js +623 -0
- package/dist/adapt/ids.d.ts +22 -0
- package/dist/adapt/ids.js +34 -0
- package/dist/adapt/instanceLedger.d.ts +36 -0
- package/dist/adapt/instanceLedger.js +50 -0
- package/dist/adapt/panelTasks.d.ts +59 -0
- package/dist/adapt/panelTasks.js +193 -0
- package/dist/adapt/textStream.d.ts +63 -0
- package/dist/adapt/textStream.js +141 -0
- package/dist/adapt/toolCards.d.ts +65 -0
- package/dist/adapt/toolCards.js +100 -0
- package/dist/adapt/turnFlags.d.ts +37 -0
- package/dist/adapt/turnFlags.js +55 -0
- package/dist/adapt/wireShapes.d.ts +96 -0
- package/dist/adapt/wireShapes.js +167 -0
- package/dist/adapt.d.ts +33 -60
- package/dist/adapt.js +84 -1207
- package/dist/adapter/downstream/eventToSdkMessage.d.ts +57 -13
- package/dist/adapter/downstream/eventToSdkMessage.js +174 -106
- package/dist/adapter/downstream/terminalToSdkResult.js +151 -160
- package/dist/adapter/downstream/turnUsageToModelUsage.d.ts +36 -0
- package/dist/adapter/downstream/turnUsageToModelUsage.js +34 -6
- package/dist/adapter/runStream.d.ts +36 -4
- package/dist/adapter/runStream.js +195 -13
- package/dist/adapter/types.d.ts +28 -2
- package/dist/agentSession/backgroundView.js +4 -15
- package/dist/agentsWireCaps.d.ts +10 -6
- package/dist/agentsWireCaps.js +21 -7
- package/dist/argvFlagValue.d.ts +41 -0
- package/dist/argvFlagValue.js +69 -0
- package/dist/attachmentsWireCaps.d.ts +3 -2
- package/dist/attachmentsWireCaps.js +5 -3
- package/dist/classifierVerdictWire.d.ts +8 -0
- package/dist/classifierVerdictWire.js +8 -0
- package/dist/cloudConfigWireCaps.d.ts +28 -1
- package/dist/cloudConfigWireCaps.js +51 -11
- package/dist/controlRouter.d.ts +14 -8
- package/dist/controlRouter.js +15 -21
- package/dist/detachWire.d.ts +15 -5
- package/dist/detachWire.js +17 -7
- package/dist/effortWire.d.ts +0 -21
- package/dist/effortWire.js +6 -20
- package/dist/engineInlineTaskStats.d.ts +12 -6
- package/dist/engineWireSdk.d.ts +12 -0
- package/dist/env/localeGeo.js +2 -1
- package/dist/envFlag.d.ts +39 -0
- package/dist/envFlag.js +51 -0
- package/dist/finalVerifyWire.d.ts +7 -5
- package/dist/finalVerifyWire.js +6 -5
- package/dist/fleet/fleetLedger.d.ts +32 -9
- package/dist/fleet/fleetLedger.js +119 -34
- package/dist/fleet/fleetProjection.d.ts +44 -6
- package/dist/fleet/fleetProjection.js +52 -10
- package/dist/fleetAgentPanelProjection.d.ts +18 -1
- package/dist/fleetAgentPanelProjection.js +61 -14
- package/dist/forkWireCaps.d.ts +2 -1
- package/dist/forkWireCaps.js +5 -11
- package/dist/goalStopHook.d.ts +142 -0
- package/dist/goalStopHook.js +258 -0
- package/dist/headlessPermissionModeWire.d.ts +10 -0
- package/dist/headlessPermissionModeWire.js +24 -21
- package/dist/headlessReconnectWire.d.ts +7 -1
- package/dist/headlessReconnectWire.js +20 -2
- package/dist/hitl/approvalsFeed.d.ts +22 -1
- package/dist/hitl/approvalsFeed.js +103 -9
- package/dist/hitl/askGateWire.d.ts +27 -96
- package/dist/hitl/askGateWire.js +69 -618
- package/dist/hitl/frameRouter.d.ts +86 -0
- package/dist/hitl/frameRouter.js +383 -0
- package/dist/hitl/gateLedger.d.ts +119 -0
- package/dist/hitl/gateLedger.js +113 -0
- package/dist/hitl/hitlBridge.d.ts +75 -15
- package/dist/hitl/hitlBridge.js +94 -22
- package/dist/hitl/hitlHostSurface.d.ts +49 -0
- package/dist/hitl/hitlHostSurface.js +155 -0
- package/dist/hitl/parkResolver.d.ts +74 -0
- package/dist/hitl/parkResolver.js +250 -0
- package/dist/hitl/planReviewWire.d.ts +60 -2
- package/dist/hitl/planReviewWire.js +197 -91
- package/dist/hitl/toolApprovalWire.d.ts +67 -4
- package/dist/hitl/toolApprovalWire.js +129 -31
- package/dist/hooksWireCaps.d.ts +1 -82
- package/dist/hooksWireCaps.js +44 -237
- package/dist/host.d.ts +16 -5
- package/dist/index.d.ts +2 -0
- package/dist/index.js +15 -1
- package/dist/interactiveToolsWire.d.ts +15 -4
- package/dist/interactiveToolsWire.js +24 -26
- package/dist/limitsWire.js +7 -40
- package/dist/liveInitToolFace.d.ts +51 -6
- package/dist/liveQuestionStore.d.ts +18 -6
- package/dist/liveQuestionStore.js +15 -0
- package/dist/model/providerPresets.js +11 -1
- package/dist/notifications.d.ts +62 -2
- package/dist/notifications.js +306 -49
- package/dist/printToolResultFrame.d.ts +12 -4
- package/dist/printToolResultFrame.js +11 -21
- package/dist/retainBackgroundWireCaps.d.ts +3 -2
- package/dist/retainBackgroundWireCaps.js +5 -9
- package/dist/sandboxWire.d.ts +9 -31
- package/dist/sandboxWire.js +51 -50
- package/dist/scenarioWire.d.ts +1 -1
- package/dist/scenarioWire.js +25 -36
- package/dist/seam.d.ts +23 -6
- package/dist/seam.js +40 -30
- package/dist/seatContract.d.ts +369 -83
- package/dist/seatContract.js +585 -198
- package/dist/selfOrchestrationWireCaps.d.ts +6 -5
- package/dist/selfOrchestrationWireCaps.js +8 -12
- package/dist/sessionSlot.d.ts +8 -0
- package/dist/sessionSlot.js +1 -0
- package/dist/steering.js +2 -2
- package/dist/subagent/engineTaskHandleWire.d.ts +3 -0
- package/dist/subagent/engineTaskHandleWire.js +17 -2
- package/dist/subagentContentStore.d.ts +5 -5
- package/dist/toolResult.d.ts +89 -8
- package/dist/toolResult.js +99 -30
- package/dist/typePins.d.ts +17 -0
- package/dist/typePins.js +1 -0
- package/dist/ultracodeWireCaps.js +5 -6
- package/dist/unrefTimer.d.ts +30 -0
- package/dist/unrefTimer.js +5 -0
- package/dist/workflow.d.ts +3 -2
- package/dist/workflow.js +3 -2
- package/dist/workflowClient.d.ts +7 -0
- package/dist/workflowClient.js +47 -12
- package/package.json +3 -3
package/dist/seatContract.js
CHANGED
|
@@ -12,10 +12,11 @@
|
|
|
12
12
|
* 落点为什么是本包:desktop 与 web 都已经依赖 `@sema-agent/client-core`(0.11.5),而本文件是
|
|
13
13
|
* **纯类型 + 常量 + 纯谓词**,零值级 import、零 Node 内建 ⇒ 不动本包的可移植闭包(portability 门的
|
|
14
14
|
* `EXPECTED_PACKAGES_INDEX` 一个字不变)。
|
|
15
|
-
* ⚠️
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* ⚠️ 两条 `import type`(`./effortWire.js` 的 `ReasoningEffort`、`./seam.js` 的 `LaneProof`)都是
|
|
16
|
+
* **类型边**:portability 门走值级传递闭包时按规则剥掉 type-only import(门里 `/^\s*type\b/` 那一跳),
|
|
17
|
+
* bundle 里一个字节都不多。写成 import 而不是就地复刻,是因为「座位闭集」与「wire 投影闭集」必须是
|
|
18
|
+
* **同一套词**——复刻一份就等于埋一条只有真用户能发现的漂移(拨盘上的档在座位 lane 上静默到不了引擎;
|
|
19
|
+
* 车道证明在座位面变成一个「有位无证」的空对象)。
|
|
19
20
|
*
|
|
20
21
|
* ── 搬迁时按「真源为准」收敛的漂移(逐条,web 侧镜像 → 本文件)────────────────────────────
|
|
21
22
|
* 🔴 D1 `LocalSessionEvent.outcome` —— web 镜像整位缺席 ⇒ `tool_permission_resolved` 只会清面,
|
|
@@ -24,7 +25,7 @@
|
|
|
24
25
|
* 而宿主入站门 `requireOptionalPermissionMode` 是**会抛的闭集门**。今日两边词表逐字相等
|
|
25
26
|
* (web `ComposerMode` 五档 ≡ `PERMISSION_MODE_INTENTS` 五档,已核),所以是**潜伏**不是现症:
|
|
26
27
|
* ComposerMode 哪天加一档,裸 string 一路放行到宿主门口才抛。收敛成闭集后当天编译红。
|
|
27
|
-
* D3 `AttachmentUploadReceipt.sha256` / `ToolPermissionRequest.
|
|
28
|
+
* D3 `AttachmentUploadReceipt.sha256` / `ToolPermissionRequest.createdAtMs` / `StartSessionOptions.cwd`
|
|
28
29
|
* / `.excludeTools` / `LocalSessionRecord.queuedMessages`·`model`·`engineSessionId` ——
|
|
29
30
|
* web 镜像缺位。都是「没接线」不是「接错线」(web 侧今日无消费点,已核),收敛后位在、待接。
|
|
30
31
|
* D4 `LocalSessionEvent.request` —— 真源有这个位,**宿主从不发**(session-host 零处写 `request:`);
|
|
@@ -37,6 +38,23 @@
|
|
|
37
38
|
* focusedSessionState / settingsState / `CHROME_EVENT_KINDS` / `ATTACHMENT_IDS_PER_TASK_CAP`。
|
|
38
39
|
* 本文件导出后它们对 web 首次可见(接不接线是渲染面的决定)。
|
|
39
40
|
*
|
|
41
|
+
* ── 🔴 wave2 卡E-seat 的**破坏性契约变更**(2026-08-02,REF-CC-064~070/072~074;发包说明点名)──
|
|
42
|
+
* ① `EipcMethodSpec.validate` / `EipcStoreSpec.validate` → **改名 `check`**,返回形从 `boolean`
|
|
43
|
+
* 升成 `true | SeatValidationFailure`(REF-CC-069)。改名是**安全机制不是洁癖**:老消费点写的是
|
|
44
|
+
* `if (m.validate && !m.validate(v))`,而失败**对象是真值** ⇒ 那一行会把「不合格」读成「合格」,
|
|
45
|
+
* 门从 fail-closed 静默变成 fail-open。改名让每个旧调用点在编译期红,而不是在运行期悄悄放行。
|
|
46
|
+
* ② `ToolPermissionRequest.createdAt: number` → **`createdAtMs`**(REF-CC-067):同一文件里
|
|
47
|
+
* `LocalSessionRecord.createdAt` 是 ISO 串,两个同名位两种域两种类型;epoch 单位入名(A4)。
|
|
48
|
+
* ③ `ToolPermissionRequest.boundCallId` + `.boundInputHash` → **合成 `toctouBinding`**
|
|
49
|
+
* (REF-CC-070):注释一直把它称作 pair,而类型/门此前各查各的,半个 pair 照样过门。
|
|
50
|
+
* ④ `LocalSessionEvent.chrome.laneProof` **可选 → 必填**且升成命名类型 `LaneProof`(REF-CC-064)——
|
|
51
|
+
* 出站门本来就要求它在场,类型却说可以缺席:一个照着这份「单一真源」写的宿主编译全绿、
|
|
52
|
+
* 事件被整条丢掉、而门只回 boolean 说不出为什么。这条是本卡的 bug 修复位。
|
|
53
|
+
* ⑤ `LocalSessionEvent.type` / `.chrome.kind` / `.permissionMode` 三个位闭集化(REF-CC-065)。
|
|
54
|
+
* ⑥ `uploadAttachment` 入参、`getTranscript`/`searchSessions` 返回形、三个 store 状态形全部提成
|
|
55
|
+
* 具名 interface(REF-CC-072/073/074),SPEC 里 13 个内联匿名 validator 收成具名 `check*`。
|
|
56
|
+
* 三端消费坐标见本批 commit message 的「候提货单」节。
|
|
57
|
+
*
|
|
40
58
|
* ── 沿革(以下正文自 sema-desktop/src/shared/localSessions.ts 逐字迁入)──────────────────────
|
|
41
59
|
* LocalSessions — the desktop host's session service contract, FORM-ported from the CC desktop
|
|
42
60
|
* app's `claude.web / LocalSessions` eipc interface (212 methods there; FIRST BATCH here = the four
|
|
@@ -54,6 +72,56 @@
|
|
|
54
72
|
* neutral typed position. Our implementation体 differs (sema wire via @sema-agent/sdk +
|
|
55
73
|
* @sema-agent/client-core), the contract shape does not.
|
|
56
74
|
*/
|
|
75
|
+
const bad = (reason, field) => field === undefined ? { reason } : { reason, field };
|
|
76
|
+
/** 存在性判定(§B10:`!!v` 那种真值判定会把空串/0 当缺席,对象位则会把 `{}` 当「有证」)。 */
|
|
77
|
+
const isObject = (v) => typeof v === "object" && v !== null;
|
|
78
|
+
const isAbsent = (v) => typeof v === "undefined";
|
|
79
|
+
const optionalString = (v) => isAbsent(v) || typeof v === "string";
|
|
80
|
+
const inTuple = (tuple, v) => tuple.includes(v);
|
|
81
|
+
export const TOCTOU_BINDING_KEYS = ["callId", "inputHash"];
|
|
82
|
+
/**
|
|
83
|
+
* 座位事件名的**闭集**(REF-CC-065):这是三端唯一的事件名真源。
|
|
84
|
+
*
|
|
85
|
+
* 方向判据(本文件 `PERMISSION_MODE_INTENTS` 头注立的那条):词由**我们自己的宿主**铸出、发往渲染端,
|
|
86
|
+
* 因此闭集 —— 拼错一个字母在编译期红,而不是变成一条谁也不认识、被 switch 默默丢掉的事件。
|
|
87
|
+
* 与 `outcome`(wire 铸词,开集)的区别正是「谁铸的词」,不是「字段像不像枚举」。
|
|
88
|
+
* 词表 = sema-desktop `session-host` 的 emit 面实测八词。
|
|
89
|
+
*/
|
|
90
|
+
const SEAT_EVENT_TYPE_TUPLE = [
|
|
91
|
+
"start",
|
|
92
|
+
"close",
|
|
93
|
+
"session_updated",
|
|
94
|
+
"message",
|
|
95
|
+
"stream_event",
|
|
96
|
+
"chrome_event",
|
|
97
|
+
"error",
|
|
98
|
+
"tool_permission_resolved",
|
|
99
|
+
];
|
|
100
|
+
export const SEAT_EVENT_TYPES = SEAT_EVENT_TYPE_TUPLE;
|
|
101
|
+
/** Chrome arms wired to the renderer position (batch 4); other arms stay host-side on the ring. */
|
|
102
|
+
const CHROME_EVENT_KIND_TUPLE = [
|
|
103
|
+
"panel_task",
|
|
104
|
+
"notification_terminal",
|
|
105
|
+
"prompt_suggestions",
|
|
106
|
+
"subagent_lifecycle",
|
|
107
|
+
];
|
|
108
|
+
/**
|
|
109
|
+
* 🔴 BREAKING(REF-CC-065,2026-08-02):公开面类型从 `readonly string[]` **收窄**成
|
|
110
|
+
* `readonly ChromeEventKind[]`。与 `PERMISSION_MODE_INTENTS` / `SEAT_REASONING_EFFORTS` 那两条
|
|
111
|
+
* **不同**:那两个本来就已经是闭集类型,加元组只是补「类型→常量」那半边、公开面一个字没变;
|
|
112
|
+
* 这一个的公开面**真的变了**,所以它是本批第四处破坏性契约变更,不是「同上理由」的搭车项。
|
|
113
|
+
*
|
|
114
|
+
* 谁受影响(实测消费坐标,提货单同条):`sema-desktop/src/main/session-host.ts:1752`
|
|
115
|
+
* `CHROME_EVENT_KINDS.includes(event.kind)` —— 那里的 `event: ChromeEvent` 是 seam.ts 的 17 词
|
|
116
|
+
* 判别式联合,收窄后 `includes()` 的入参类型只认这 4 个词 ⇒ TS2345,提货当天编译红。
|
|
117
|
+
* 迁移形:`(CHROME_EVENT_KINDS as readonly string[]).includes(event.kind)`,或先按 `ChromeEventKind`
|
|
118
|
+
* 做一次显式收窄再进这一支。sema-web-client / sema-cli 实测零命中。
|
|
119
|
+
*/
|
|
120
|
+
export const CHROME_EVENT_KINDS = CHROME_EVENT_KIND_TUPLE;
|
|
121
|
+
/** `LaneProof` 的判别位词表 —— 与 `seam.ts` 的联合逐词对账(下方 `_seatContractPins` 双向钉:
|
|
122
|
+
* seam 加一条车道而这里不跟 ⇒ 编译红,而不是运行期把新车道的 chrome 帧整条丢掉)。 */
|
|
123
|
+
const LANE_PROOF_LANE_TUPLE = ["main", "subagent", "workflow"];
|
|
124
|
+
export const SEAT_CHROME_ENVELOPE_KEYS = ["kind", "laneProof"];
|
|
57
125
|
/**
|
|
58
126
|
* The resolution words the host currently puts on `tool_permission_resolved.outcome` ([1947] 红③).
|
|
59
127
|
* DOCUMENTARY + test anchor only — the gate does NOT enforce membership (see the field's doc: a
|
|
@@ -72,48 +140,6 @@ export const HITL_RESOLUTION_OUTCOMES = [
|
|
|
72
140
|
"decline",
|
|
73
141
|
"cancel",
|
|
74
142
|
];
|
|
75
|
-
/**
|
|
76
|
-
* 接缝记账 — what the HOST now guarantees vs what a RENDERER still owes ([1947] 红③/红④ split).
|
|
77
|
-
*
|
|
78
|
-
* The host half is done and pinned by desktop's `scripts/run-hitl-frame-transit-fence-test.mjs`:
|
|
79
|
-
* every live ask family reaches `onToolPermissionRequest` with its own requestId domain, every
|
|
80
|
-
* resolution reaches `onEvent` with an `outcome`, and every decision routes to the right wire verb.
|
|
81
|
-
*
|
|
82
|
-
* The renderer half is where these become VISIBLE, and until it lands the data is present but
|
|
83
|
-
* unread (stating it plainly so nobody reads "the host emits it" as "the user sees it"):
|
|
84
|
-
* 1. `tool_permission_resolved.outcome` — an `expired` / `denied` / `unanswered` resolution must
|
|
85
|
-
* leave a visible refusal trace. Today the seat transport clears the surface and drops the
|
|
86
|
-
* word, so a refusal is still indistinguishable from an approval on screen.
|
|
87
|
-
* 🔴 B18(2026-07-29)查明这条一直不销账的**机制原因**:web 侧的镜像类型里连 `outcome` 这个
|
|
88
|
-
* 位都没有,所以「没读」在两边都编译得过。本批把位收进真源 = 让它至少可见于类型面。
|
|
89
|
-
* 2. `toolCallId` — now real (not null), so an approval can be anchored to the tool card that
|
|
90
|
-
* raised it instead of "the most recent tool_start" (an anchor that lands on the wrong card
|
|
91
|
-
* when a prior tool is still running).
|
|
92
|
-
* 3. `question:` rows need NO new renderer code: they arrive as `toolName: "AskUserQuestion"` with
|
|
93
|
-
* `input.questions`, which is exactly what the seat's ask-user-question surface already reads,
|
|
94
|
-
* and its answer envelope is exactly what this host expects back.
|
|
95
|
-
* 4. 座位 wire 补齐批 (2026-07-28) closed two entries that used to live here as "如实,未做":
|
|
96
|
-
* `permissionMode` now has a seat on `sendMessage` (was: the composer's Plan badge never
|
|
97
|
-
* crossed and the turn ran with the engine default), and the attachment BYTE channel exists
|
|
98
|
-
* (`uploadAttachment` + `sendMessage.attachmentIds`) — so a seat renderer no longer has to
|
|
99
|
-
* simulate "attachment ready" for a file nothing received. What is still NOT here, stated
|
|
100
|
-
* plainly: plan REVISE feedback text (the decide face has no `reason` seat, so revise maps to
|
|
101
|
-
* a bare deny) and a per-session `permissionMode` write (the wire's is per-TURN only).
|
|
102
|
-
* 5. `elicit:` rows have no dedicated surface yet: they fall through to the generic approval card,
|
|
103
|
-
* which shows the MCP server's prompt and can accept/decline but CANNOT collect form values.
|
|
104
|
-
* A real form surface (built from `input.requestedSchema`) is the renderer's remaining piece;
|
|
105
|
-
* until then an approve carries no content — the host never invents form values. That generic
|
|
106
|
-
* card also offers "Always allow (this session)", which this domain has no concept of: the host
|
|
107
|
-
* ignores `remember` here rather than pretend, so the renderer should hide that control for
|
|
108
|
-
* `elicit:` rows (a button that changes nothing is the false-affordance family again).
|
|
109
|
-
*/
|
|
110
|
-
/** Chrome arms wired to the renderer position (batch 4); other arms stay host-side on the ring. */
|
|
111
|
-
export const CHROME_EVENT_KINDS = [
|
|
112
|
-
"panel_task",
|
|
113
|
-
"notification_terminal",
|
|
114
|
-
"prompt_suggestions",
|
|
115
|
-
"subagent_lifecycle",
|
|
116
|
-
];
|
|
117
143
|
/** The closed set the inbound gate checks. CLOSED deliberately, unlike `outcome` above: this word
|
|
118
144
|
* ORIGINATES in our own renderer and goes OUT to a wire that 400s on anything else, so an unknown
|
|
119
145
|
* value is a local bug we want reported at the door — not an unknown future wire word we must
|
|
@@ -149,186 +175,363 @@ const SEAT_REASONING_EFFORT_TUPLE = [
|
|
|
149
175
|
];
|
|
150
176
|
export const SEAT_REASONING_EFFORTS = SEAT_REASONING_EFFORT_TUPLE;
|
|
151
177
|
export const SEAT_TURN_SETTINGS_KEYS = ["ultracode"];
|
|
178
|
+
export const ATTACHMENT_UPLOAD_REQUEST_KEYS = ["name", "mimeType", "bytes"];
|
|
179
|
+
export const SESSION_SEARCH_RESULT_KEYS = ["sessionId", "title", "matches"];
|
|
180
|
+
export const GET_TRANSCRIPT_RESULT_KEYS = ["sessionId", "messages"];
|
|
152
181
|
// ───────────────────────────── validators (CC dual-gate posture) ─────────────────────────────
|
|
182
|
+
//
|
|
183
|
+
// 一种方言(REF-CC-072):每个载荷形恰有一个具名 `check*`(返回 `true | {reason, field?}`),
|
|
184
|
+
// 需要类型收窄的地方再包一层 `is*` 谓词。SPEC 里不再有内联匿名 validator。
|
|
185
|
+
// 「哪些键真被校验、哪些是刻意豁免」由 `SEAT_VALIDATOR_KEY_COVERAGE` 登记,门里逐位投毒对账
|
|
186
|
+
// (REF-CC-068):说校验的位投毒必拒,说豁免的位投毒必过 —— 表和码不可能各说各话。
|
|
187
|
+
const checkLocalSessionRecord = (s) => {
|
|
188
|
+
if (!isObject(s))
|
|
189
|
+
return bad("session record is not an object");
|
|
190
|
+
if (typeof s.sessionId !== "string")
|
|
191
|
+
return bad("sessionId must be a string", "sessionId");
|
|
192
|
+
if (!optionalString(s.engineSessionId))
|
|
193
|
+
return bad("engineSessionId must be a string when present", "engineSessionId");
|
|
194
|
+
if (!optionalString(s.model))
|
|
195
|
+
return bad("model must be a string when present", "model");
|
|
196
|
+
if (!optionalString(s.title))
|
|
197
|
+
return bad("title must be a string when present", "title");
|
|
198
|
+
if (typeof s.isRunning !== "boolean")
|
|
199
|
+
return bad("isRunning must be a boolean", "isRunning");
|
|
200
|
+
if (typeof s.createdAt !== "string")
|
|
201
|
+
return bad("createdAt must be an ISO-8601 string", "createdAt");
|
|
202
|
+
if (!optionalString(s.lastActivityAt))
|
|
203
|
+
return bad("lastActivityAt must be a string when present", "lastActivityAt");
|
|
204
|
+
if (!isAbsent(s.queuedMessages)) {
|
|
205
|
+
const q = s.queuedMessages;
|
|
206
|
+
const okQueue = Array.isArray(q) && q.every((row) => isObject(row) && typeof row.uuid === "string" && typeof row.text === "string");
|
|
207
|
+
if (!okQueue)
|
|
208
|
+
return bad("queuedMessages must be an array of {uuid,text}", "queuedMessages");
|
|
209
|
+
}
|
|
210
|
+
return true;
|
|
211
|
+
};
|
|
153
212
|
export function isLocalSessionRecord(s) {
|
|
154
|
-
|
|
155
|
-
return !(!e ||
|
|
156
|
-
typeof e !== "object" ||
|
|
157
|
-
typeof e.sessionId !== "string" ||
|
|
158
|
-
(typeof e.engineSessionId !== "undefined" && typeof e.engineSessionId !== "string") ||
|
|
159
|
-
(typeof e.model !== "undefined" && typeof e.model !== "string") ||
|
|
160
|
-
(typeof e.title !== "undefined" && typeof e.title !== "string") ||
|
|
161
|
-
typeof e.isRunning !== "boolean" ||
|
|
162
|
-
typeof e.createdAt !== "string" ||
|
|
163
|
-
(typeof e.lastActivityAt !== "undefined" && typeof e.lastActivityAt !== "string") ||
|
|
164
|
-
(typeof e.queuedMessages !== "undefined" &&
|
|
165
|
-
!(Array.isArray(e.queuedMessages) &&
|
|
166
|
-
e.queuedMessages.every((q) => !!q && typeof q.uuid === "string" && typeof q.text === "string"))));
|
|
213
|
+
return checkLocalSessionRecord(s) === true;
|
|
167
214
|
}
|
|
215
|
+
const checkToctouBinding = (v) => {
|
|
216
|
+
if (!isObject(v))
|
|
217
|
+
return bad("toctouBinding must be an object", "toctouBinding");
|
|
218
|
+
// 🔴 REF-CC-070:pair 的两半在同一个判据里 —— 只剩一半的绑定不是「降级」,是半个绑定。
|
|
219
|
+
if (typeof v.callId !== "string")
|
|
220
|
+
return bad("toctouBinding.callId must be a string", "toctouBinding.callId");
|
|
221
|
+
if (typeof v.inputHash !== "string")
|
|
222
|
+
return bad("toctouBinding.inputHash must be a string", "toctouBinding.inputHash");
|
|
223
|
+
return true;
|
|
224
|
+
};
|
|
225
|
+
const checkToolPermissionRequest = (r) => {
|
|
226
|
+
if (!isObject(r))
|
|
227
|
+
return bad("tool permission request is not an object");
|
|
228
|
+
if (typeof r.requestId !== "string")
|
|
229
|
+
return bad("requestId must be a string", "requestId");
|
|
230
|
+
if (typeof r.sessionId !== "string")
|
|
231
|
+
return bad("sessionId must be a string", "sessionId");
|
|
232
|
+
if (!isAbsent(r.toolName) && r.toolName !== null && typeof r.toolName !== "string") {
|
|
233
|
+
return bad("toolName must be a string or null when present", "toolName");
|
|
234
|
+
}
|
|
235
|
+
if (!isAbsent(r.toolCallId) && r.toolCallId !== null && typeof r.toolCallId !== "string") {
|
|
236
|
+
return bad("toolCallId must be a string or null when present", "toolCallId");
|
|
237
|
+
}
|
|
238
|
+
// `input` is the untrusted redacted payload — passthrough (render-only), like CC's request args.
|
|
239
|
+
if (!optionalString(r.message))
|
|
240
|
+
return bad("message must be a string when present", "message");
|
|
241
|
+
if (!isAbsent(r.toctouBinding)) {
|
|
242
|
+
const verdict = checkToctouBinding(r.toctouBinding);
|
|
243
|
+
if (verdict !== true)
|
|
244
|
+
return verdict;
|
|
245
|
+
}
|
|
246
|
+
if (!isAbsent(r.createdAtMs) && typeof r.createdAtMs !== "number") {
|
|
247
|
+
return bad("createdAtMs must be a number (epoch ms) when present", "createdAtMs");
|
|
248
|
+
}
|
|
249
|
+
return true;
|
|
250
|
+
};
|
|
168
251
|
export function isToolPermissionRequest(r) {
|
|
169
|
-
|
|
170
|
-
return !(!e ||
|
|
171
|
-
typeof e !== "object" ||
|
|
172
|
-
typeof e.requestId !== "string" ||
|
|
173
|
-
typeof e.sessionId !== "string" ||
|
|
174
|
-
(typeof e.toolName !== "undefined" && e.toolName !== null && typeof e.toolName !== "string") ||
|
|
175
|
-
(typeof e.toolCallId !== "undefined" && e.toolCallId !== null && typeof e.toolCallId !== "string") ||
|
|
176
|
-
// `input` is the untrusted redacted payload — passthrough (render-only), like CC's request args.
|
|
177
|
-
(typeof e.message !== "undefined" && typeof e.message !== "string") ||
|
|
178
|
-
(typeof e.boundCallId !== "undefined" && typeof e.boundCallId !== "string") ||
|
|
179
|
-
(typeof e.boundInputHash !== "undefined" && typeof e.boundInputHash !== "string") ||
|
|
180
|
-
(typeof e.createdAt !== "undefined" && typeof e.createdAt !== "number"));
|
|
252
|
+
return checkToolPermissionRequest(r) === true;
|
|
181
253
|
}
|
|
254
|
+
/** 车道证明的结构校验(REF-CC-064 的另一半):此前门写的是 `!!laneProof` —— 真值判定当存在性判定
|
|
255
|
+
* (§B10),于是 `laneProof: {}` 这种「有位无证」的帧照样过门,而下游按 lane 分发时它谁也不是。 */
|
|
256
|
+
const checkLaneProof = (v, field) => {
|
|
257
|
+
if (!isObject(v))
|
|
258
|
+
return bad("laneProof must be an object", field);
|
|
259
|
+
const lane = v.lane;
|
|
260
|
+
if (typeof lane !== "string" || !inTuple(LANE_PROOF_LANE_TUPLE, lane)) {
|
|
261
|
+
return bad(`laneProof.lane must be one of ${LANE_PROOF_LANE_TUPLE.join("|")}`, `${field}.lane`);
|
|
262
|
+
}
|
|
263
|
+
if (lane === "subagent" && typeof v.parentToolCallId !== "string") {
|
|
264
|
+
return bad("subagent laneProof needs parentToolCallId", `${field}.parentToolCallId`);
|
|
265
|
+
}
|
|
266
|
+
if (lane === "workflow" && (typeof v.via !== "string" || typeof v.key !== "string")) {
|
|
267
|
+
return bad("workflow laneProof needs via + key", `${field}.via`);
|
|
268
|
+
}
|
|
269
|
+
return true;
|
|
270
|
+
};
|
|
271
|
+
const checkSeatChromeEnvelope = (v, field) => {
|
|
272
|
+
if (!isObject(v))
|
|
273
|
+
return bad("chrome must be an object", field);
|
|
274
|
+
const kind = v.kind;
|
|
275
|
+
if (typeof kind !== "string")
|
|
276
|
+
return bad("chrome.kind must be a string", `${field}.kind`);
|
|
277
|
+
if (!inTuple(CHROME_EVENT_KIND_TUPLE, kind)) {
|
|
278
|
+
return bad(`chrome.kind is not a renderer-wired arm (${CHROME_EVENT_KIND_TUPLE.join("|")})`, `${field}.kind`);
|
|
279
|
+
}
|
|
280
|
+
return checkLaneProof(v.laneProof, `${field}.laneProof`);
|
|
281
|
+
};
|
|
182
282
|
/**
|
|
183
283
|
* onEvent payload validator — line-for-line FORM port of CC's `fQe`
|
|
184
284
|
* (`.vite/build/index.chunk-CnWKsyE_.js:369773`). Field-by-field mapping:
|
|
185
|
-
* - `type`/`sessionId`: required strings (same);
|
|
285
|
+
* - `type`/`sessionId`: required strings (same);`type` 另加闭集成员校验(REF-CC-065,词由本宿主铸);
|
|
186
286
|
* - `message`: CC's minified source reads `typeof e.message<"u"` inside a comma expression whose
|
|
187
287
|
* value is discarded — i.e. NO validation, a deliberate transcript-plane passthrough. Same here.
|
|
188
288
|
* - `messages`: array-checked, elements `every(t=>!0)` (constant true) — array shape only. Same here.
|
|
189
289
|
* - `error`/`errorCategory`: optional strings (same); `code`: optional number (same);
|
|
190
|
-
* - `request`: CC gates with `S1` (ToolPermissionRequest schema) → our
|
|
191
|
-
* - `session`: CC gates with `aM` (SessionRecord schema) → our
|
|
192
|
-
* - `data`: optional string / `sources`: optional string[] / `permissionMode`:
|
|
290
|
+
* - `request`: CC gates with `S1` (ToolPermissionRequest schema) → our checkToolPermissionRequest;
|
|
291
|
+
* - `session`: CC gates with `aM` (SessionRecord schema) → our checkLocalSessionRecord;
|
|
292
|
+
* - `data`: optional string / `sources`: optional string[] / `permissionMode`: 闭集成员 /
|
|
193
293
|
* `userMessageUuid`: optional string / `hasPreClearSession`: optional boolean (same);
|
|
194
294
|
* - CC fields we do not emit in this batch (`tccFolderKind`/`initializationStatus`/`fsFile`/
|
|
195
295
|
* `localMcpServers`/`refusalFallbackPrompt`) are not carried — their positions return when the
|
|
196
296
|
* corresponding service families are ported.
|
|
197
297
|
*/
|
|
298
|
+
const checkLocalSessionEvent = (v) => {
|
|
299
|
+
if (!isObject(v))
|
|
300
|
+
return bad("event is not an object");
|
|
301
|
+
const type = v.type;
|
|
302
|
+
if (typeof type !== "string")
|
|
303
|
+
return bad("type must be a string", "type");
|
|
304
|
+
if (!inTuple(SEAT_EVENT_TYPE_TUPLE, type)) {
|
|
305
|
+
return bad(`type is not a registered seat event name (${SEAT_EVENT_TYPE_TUPLE.join("|")})`, "type");
|
|
306
|
+
}
|
|
307
|
+
if (typeof v.sessionId !== "string")
|
|
308
|
+
return bad("sessionId must be a string", "sessionId");
|
|
309
|
+
// `message`: intentionally unvalidated passthrough (see doc above — CC fQe does the same).
|
|
310
|
+
if (!isAbsent(v.messages) && !Array.isArray(v.messages))
|
|
311
|
+
return bad("messages must be an array", "messages");
|
|
312
|
+
if (!optionalString(v.error))
|
|
313
|
+
return bad("error must be a string when present", "error");
|
|
314
|
+
if (!optionalString(v.errorCategory))
|
|
315
|
+
return bad("errorCategory must be a string when present", "errorCategory");
|
|
316
|
+
if (!isAbsent(v.code) && typeof v.code !== "number")
|
|
317
|
+
return bad("code must be a number when present", "code");
|
|
318
|
+
if (!isAbsent(v.request)) {
|
|
319
|
+
const verdict = checkToolPermissionRequest(v.request);
|
|
320
|
+
if (verdict !== true)
|
|
321
|
+
return bad(`request: ${verdict.reason}`, "request");
|
|
322
|
+
}
|
|
323
|
+
if (!optionalString(v.data))
|
|
324
|
+
return bad("data must be a string when present", "data");
|
|
325
|
+
if (!isAbsent(v.sources)) {
|
|
326
|
+
const okSources = Array.isArray(v.sources) && v.sources.every((t) => typeof t === "string");
|
|
327
|
+
if (!okSources)
|
|
328
|
+
return bad("sources must be a string[]", "sources");
|
|
329
|
+
}
|
|
330
|
+
if (!isAbsent(v.permissionMode)) {
|
|
331
|
+
const mode = v.permissionMode;
|
|
332
|
+
if (typeof mode !== "string" || !inTuple(PERMISSION_MODE_INTENT_TUPLE, mode)) {
|
|
333
|
+
return bad(`permissionMode must be one of ${PERMISSION_MODE_INTENT_TUPLE.join("|")}`, "permissionMode");
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (!isAbsent(v.session)) {
|
|
337
|
+
const verdict = checkLocalSessionRecord(v.session);
|
|
338
|
+
if (verdict !== true)
|
|
339
|
+
return bad(`session: ${verdict.reason}`, "session");
|
|
340
|
+
}
|
|
341
|
+
if (!optionalString(v.userMessageUuid))
|
|
342
|
+
return bad("userMessageUuid must be a string when present", "userMessageUuid");
|
|
343
|
+
if (!isAbsent(v.hasPreClearSession) && typeof v.hasPreClearSession !== "boolean") {
|
|
344
|
+
return bad("hasPreClearSession must be a boolean when present", "hasPreClearSession");
|
|
345
|
+
}
|
|
346
|
+
if (!optionalString(v.requestId))
|
|
347
|
+
return bad("requestId must be a string when present", "requestId");
|
|
348
|
+
// `outcome`: type-checked only, NOT membership-checked (see the field doc — an unknown future
|
|
349
|
+
// wire word must reach the renderer as "settled, reason unknown", never take the event down).
|
|
350
|
+
if (!optionalString(v.outcome))
|
|
351
|
+
return bad("outcome must be a string when present", "outcome");
|
|
352
|
+
if (!isAbsent(v.chrome)) {
|
|
353
|
+
const verdict = checkSeatChromeEnvelope(v.chrome, "chrome");
|
|
354
|
+
if (verdict !== true)
|
|
355
|
+
return bad(verdict.reason, "chrome");
|
|
356
|
+
}
|
|
357
|
+
return true;
|
|
358
|
+
};
|
|
198
359
|
export function isLocalSessionEvent(v) {
|
|
199
|
-
|
|
200
|
-
return !(!e ||
|
|
201
|
-
typeof e !== "object" ||
|
|
202
|
-
typeof e.type !== "string" ||
|
|
203
|
-
typeof e.sessionId !== "string" ||
|
|
204
|
-
// `message`: intentionally unvalidated passthrough (see doc above — CC fQe does the same).
|
|
205
|
-
(typeof e.messages !== "undefined" && !Array.isArray(e.messages)) ||
|
|
206
|
-
(typeof e.error !== "undefined" && typeof e.error !== "string") ||
|
|
207
|
-
(typeof e.errorCategory !== "undefined" && typeof e.errorCategory !== "string") ||
|
|
208
|
-
(typeof e.code !== "undefined" && typeof e.code !== "number") ||
|
|
209
|
-
(typeof e.request !== "undefined" && !isToolPermissionRequest(e.request)) ||
|
|
210
|
-
(typeof e.data !== "undefined" && typeof e.data !== "string") ||
|
|
211
|
-
(typeof e.sources !== "undefined" &&
|
|
212
|
-
!(Array.isArray(e.sources) && e.sources.every((t) => typeof t === "string"))) ||
|
|
213
|
-
(typeof e.permissionMode !== "undefined" && typeof e.permissionMode !== "string") ||
|
|
214
|
-
(typeof e.session !== "undefined" && !isLocalSessionRecord(e.session)) ||
|
|
215
|
-
(typeof e.userMessageUuid !== "undefined" && typeof e.userMessageUuid !== "string") ||
|
|
216
|
-
(typeof e.hasPreClearSession !== "undefined" && typeof e.hasPreClearSession !== "boolean") ||
|
|
217
|
-
(typeof e.requestId !== "undefined" && typeof e.requestId !== "string") ||
|
|
218
|
-
// `outcome`: type-checked only, NOT membership-checked (see the field doc — an unknown future
|
|
219
|
-
// wire word must reach the renderer as "settled, reason unknown", never take the event down).
|
|
220
|
-
(typeof e.outcome !== "undefined" && typeof e.outcome !== "string") ||
|
|
221
|
-
(typeof e.chrome !== "undefined" &&
|
|
222
|
-
!(!!e.chrome &&
|
|
223
|
-
typeof e.chrome === "object" &&
|
|
224
|
-
typeof e.chrome.kind === "string" &&
|
|
225
|
-
CHROME_EVENT_KINDS.includes(e.chrome.kind) &&
|
|
226
|
-
// lane discipline first-class ([1617] family): a chrome arm always carries its LaneProof.
|
|
227
|
-
!!e.chrome.laneProof)));
|
|
360
|
+
return checkLocalSessionEvent(v) === true;
|
|
228
361
|
}
|
|
362
|
+
const checkAttachmentUploadReceipt = (v) => {
|
|
363
|
+
if (!isObject(v))
|
|
364
|
+
return bad("upload receipt is not an object");
|
|
365
|
+
if (typeof v.id !== "string")
|
|
366
|
+
return bad("id must be a string", "id");
|
|
367
|
+
if (typeof v.name !== "string")
|
|
368
|
+
return bad("name must be a string", "name");
|
|
369
|
+
if (typeof v.mime !== "string")
|
|
370
|
+
return bad("mime must be a string", "mime");
|
|
371
|
+
if (typeof v.sha256 !== "string")
|
|
372
|
+
return bad("sha256 must be a string", "sha256");
|
|
373
|
+
if (typeof v.sizeBytes !== "number")
|
|
374
|
+
return bad("sizeBytes must be a number", "sizeBytes");
|
|
375
|
+
return true;
|
|
376
|
+
};
|
|
229
377
|
export function isAttachmentUploadReceipt(v) {
|
|
230
|
-
|
|
231
|
-
return !(!e ||
|
|
232
|
-
typeof e !== "object" ||
|
|
233
|
-
typeof e.id !== "string" ||
|
|
234
|
-
typeof e.name !== "string" ||
|
|
235
|
-
typeof e.mime !== "string" ||
|
|
236
|
-
typeof e.sha256 !== "string" ||
|
|
237
|
-
typeof e.sizeBytes !== "number");
|
|
378
|
+
return checkAttachmentUploadReceipt(v) === true;
|
|
238
379
|
}
|
|
380
|
+
const checkSeatModelInfo = (m, index) => {
|
|
381
|
+
if (!isObject(m))
|
|
382
|
+
return bad(`models[${index}] is not an object`, "models");
|
|
383
|
+
if (typeof m.id !== "string")
|
|
384
|
+
return bad(`models[${index}].id must be a string`, "models");
|
|
385
|
+
if (!optionalString(m.name))
|
|
386
|
+
return bad(`models[${index}].name must be a string when present`, "models");
|
|
387
|
+
if (!optionalString(m.provider))
|
|
388
|
+
return bad(`models[${index}].provider must be a string when present`, "models");
|
|
389
|
+
if (!isAbsent(m.contextWindow) && typeof m.contextWindow !== "number") {
|
|
390
|
+
return bad(`models[${index}].contextWindow must be a number when present`, "models");
|
|
391
|
+
}
|
|
392
|
+
if (!isAbsent(m.maxOutputTokens) && typeof m.maxOutputTokens !== "number") {
|
|
393
|
+
return bad(`models[${index}].maxOutputTokens must be a number when present`, "models");
|
|
394
|
+
}
|
|
395
|
+
return true;
|
|
396
|
+
};
|
|
397
|
+
const checkSeatModelCatalog = (v) => {
|
|
398
|
+
if (!isObject(v))
|
|
399
|
+
return bad("model catalog is not an object");
|
|
400
|
+
const models = v.models;
|
|
401
|
+
if (!Array.isArray(models))
|
|
402
|
+
return bad("models must be an array", "models");
|
|
403
|
+
for (let i = 0; i < models.length; i += 1) {
|
|
404
|
+
const verdict = checkSeatModelInfo(models[i], i);
|
|
405
|
+
if (verdict !== true)
|
|
406
|
+
return verdict;
|
|
407
|
+
}
|
|
408
|
+
if (!optionalString(v.defaultId))
|
|
409
|
+
return bad("defaultId must be a string when present", "defaultId");
|
|
410
|
+
return true;
|
|
411
|
+
};
|
|
239
412
|
export function isSeatModelCatalog(v) {
|
|
240
|
-
|
|
241
|
-
return !(!e ||
|
|
242
|
-
typeof e !== "object" ||
|
|
243
|
-
!Array.isArray(e.models) ||
|
|
244
|
-
!e.models.every((m) => !!m &&
|
|
245
|
-
typeof m === "object" &&
|
|
246
|
-
typeof m.id === "string" &&
|
|
247
|
-
["undefined", "string"].includes(typeof m.name) &&
|
|
248
|
-
["undefined", "string"].includes(typeof m.provider) &&
|
|
249
|
-
["undefined", "number"].includes(typeof m.contextWindow) &&
|
|
250
|
-
["undefined", "number"].includes(typeof m.maxOutputTokens)) ||
|
|
251
|
-
(typeof e.defaultId !== "undefined" && typeof e.defaultId !== "string"));
|
|
413
|
+
return checkSeatModelCatalog(v) === true;
|
|
252
414
|
}
|
|
415
|
+
const checkSendMessageAck = (v) => {
|
|
416
|
+
if (!isObject(v))
|
|
417
|
+
return bad("sendMessage ack is not an object");
|
|
418
|
+
if (typeof v.sessionId !== "string")
|
|
419
|
+
return bad("sessionId must be a string", "sessionId");
|
|
420
|
+
if (typeof v.userMessageUuid !== "string")
|
|
421
|
+
return bad("userMessageUuid must be a string", "userMessageUuid");
|
|
422
|
+
if (!isAbsent(v.queued) && typeof v.queued !== "boolean")
|
|
423
|
+
return bad("queued must be a boolean when present", "queued");
|
|
424
|
+
return true;
|
|
425
|
+
};
|
|
253
426
|
export function isSendMessageAck(v) {
|
|
254
|
-
|
|
255
|
-
return !(!e ||
|
|
256
|
-
typeof e !== "object" ||
|
|
257
|
-
typeof e.sessionId !== "string" ||
|
|
258
|
-
typeof e.userMessageUuid !== "string" ||
|
|
259
|
-
(typeof e.queued !== "undefined" && typeof e.queued !== "boolean"));
|
|
427
|
+
return checkSendMessageAck(v) === true;
|
|
260
428
|
}
|
|
429
|
+
// ── REF-CC-072:SPEC 里此前 13 个内联匿名 validator 的具名化(同一方言,各配 interface + 键集钉)──
|
|
430
|
+
const checkInterruptAck = (v) => isObject(v) && typeof v.interrupted === "boolean" ? true : bad("interrupt ack needs boolean `interrupted`", "interrupted");
|
|
431
|
+
const checkStopAck = (v) => isObject(v) && typeof v.stopped === "boolean" ? true : bad("stop ack needs boolean `stopped`", "stopped");
|
|
432
|
+
const checkOkAck = (v) => isObject(v) && typeof v.ok === "boolean" ? true : bad("ack needs boolean `ok`", "ok");
|
|
433
|
+
/** Queue family + setModel — CC returns bare booleans (false = too-late/no-op), same here. */
|
|
434
|
+
const checkBooleanAck = (v) => typeof v === "boolean" ? true : bad("expected a bare boolean result");
|
|
435
|
+
const checkGetTranscriptResult = (v) => {
|
|
436
|
+
if (!isObject(v))
|
|
437
|
+
return bad("transcript envelope is not an object");
|
|
438
|
+
if (typeof v.sessionId !== "string")
|
|
439
|
+
return bad("sessionId must be a string", "sessionId");
|
|
440
|
+
// messages = the transcript-plane passthrough batch (fQe `messages` posture: array-checked,
|
|
441
|
+
// elements deliberately unvalidated — they are CC SDKMessage bodies).
|
|
442
|
+
if (!Array.isArray(v.messages))
|
|
443
|
+
return bad("messages must be an array", "messages");
|
|
444
|
+
return true;
|
|
445
|
+
};
|
|
446
|
+
/** 一行搜索命中(REF-CC-072 的同一方言:此前它是 envelope 校验器体内的一段内联判据 ⇒ 没有自己的
|
|
447
|
+
* 名字、没有覆盖表席位,给 `SessionSearchResult` 加一个必填位不会让任何门变红)。 */
|
|
448
|
+
const checkSessionSearchResult = (v) => {
|
|
449
|
+
if (!isObject(v))
|
|
450
|
+
return bad("search result row is not an object");
|
|
451
|
+
if (typeof v.sessionId !== "string")
|
|
452
|
+
return bad("sessionId must be a string", "sessionId");
|
|
453
|
+
// REF-CC-073:`title` 此前从不被校 —— 一个非串 title 会一路走到渲染端。
|
|
454
|
+
if (!optionalString(v.title))
|
|
455
|
+
return bad("title must be a string when present", "title");
|
|
456
|
+
if (!Array.isArray(v.matches))
|
|
457
|
+
return bad("matches must be an array", "matches");
|
|
458
|
+
const okMatches = v.matches.every((m) => isObject(m) && typeof m.snippet === "string");
|
|
459
|
+
if (!okMatches)
|
|
460
|
+
return bad("matches must be {snippet:string}[]", "matches");
|
|
461
|
+
return true;
|
|
462
|
+
};
|
|
463
|
+
const checkSessionSearchEnvelope = (v) => {
|
|
464
|
+
if (!isObject(v))
|
|
465
|
+
return bad("search envelope is not an object");
|
|
466
|
+
const results = v.results;
|
|
467
|
+
if (!Array.isArray(results))
|
|
468
|
+
return bad("results must be an array", "results");
|
|
469
|
+
for (let i = 0; i < results.length; i += 1) {
|
|
470
|
+
const verdict = checkSessionSearchResult(results[i]);
|
|
471
|
+
if (verdict !== true)
|
|
472
|
+
return bad(`results[${i}]: ${verdict.reason}`, "results");
|
|
473
|
+
}
|
|
474
|
+
return true;
|
|
475
|
+
};
|
|
476
|
+
const checkSessionsState = (v) => {
|
|
477
|
+
if (!isObject(v))
|
|
478
|
+
return bad("sessions state is not an object");
|
|
479
|
+
const sessions = v.sessions;
|
|
480
|
+
if (!Array.isArray(sessions))
|
|
481
|
+
return bad("sessions must be an array", "sessions");
|
|
482
|
+
for (let i = 0; i < sessions.length; i += 1) {
|
|
483
|
+
const verdict = checkLocalSessionRecord(sessions[i]);
|
|
484
|
+
if (verdict !== true)
|
|
485
|
+
return bad(`sessions[${i}]: ${verdict.reason}`, "sessions");
|
|
486
|
+
}
|
|
487
|
+
return true;
|
|
488
|
+
};
|
|
489
|
+
const checkFocusedSessionState = (v) => {
|
|
490
|
+
if (!isObject(v))
|
|
491
|
+
return bad("focused session state is not an object");
|
|
492
|
+
if (v.sessionId !== null && typeof v.sessionId !== "string") {
|
|
493
|
+
return bad("sessionId must be a string or null", "sessionId");
|
|
494
|
+
}
|
|
495
|
+
return true;
|
|
496
|
+
};
|
|
497
|
+
const checkSettingsState = (v) => {
|
|
498
|
+
if (!isObject(v))
|
|
499
|
+
return bad("settings state is not an object");
|
|
500
|
+
if (v.defaultModel !== null && typeof v.defaultModel !== "string") {
|
|
501
|
+
return bad("defaultModel must be a string or null", "defaultModel");
|
|
502
|
+
}
|
|
503
|
+
if (!optionalString(v.engineVersion))
|
|
504
|
+
return bad("engineVersion must be a string when present", "engineVersion");
|
|
505
|
+
if (!optionalString(v.dataRoot))
|
|
506
|
+
return bad("dataRoot must be a string when present", "dataRoot");
|
|
507
|
+
return true;
|
|
508
|
+
};
|
|
261
509
|
// ───────────────────────────── service spec (eipc registration table) ─────────────────────────────
|
|
262
510
|
export const LOCAL_SESSIONS_SPEC = {
|
|
263
511
|
ns: "sema.web",
|
|
264
512
|
service: "LocalSessions",
|
|
265
513
|
methods: {
|
|
266
|
-
start: { kind: "invoke",
|
|
267
|
-
sendMessage: { kind: "invoke",
|
|
268
|
-
interrupt: {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
},
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
},
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
},
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
reorderQueuedMessage: { kind: "invoke", validate: (v) => typeof v === "boolean" },
|
|
283
|
-
promoteQueuedMessage: { kind: "invoke", validate: (v) => typeof v === "boolean" },
|
|
284
|
-
setFocusedSession: {
|
|
285
|
-
kind: "invoke",
|
|
286
|
-
validate: (v) => !!v && typeof v.ok === "boolean",
|
|
287
|
-
},
|
|
288
|
-
setModel: { kind: "invoke", validate: (v) => typeof v === "boolean" },
|
|
289
|
-
getTranscript: {
|
|
290
|
-
kind: "invoke",
|
|
291
|
-
// messages = the transcript-plane passthrough batch (fQe `messages` posture: array-checked,
|
|
292
|
-
// elements deliberately unvalidated — they are CC SDKMessage bodies).
|
|
293
|
-
validate: (v) => !!v &&
|
|
294
|
-
typeof v.sessionId === "string" &&
|
|
295
|
-
Array.isArray(v.messages),
|
|
296
|
-
},
|
|
297
|
-
searchSessions: {
|
|
298
|
-
kind: "invoke",
|
|
299
|
-
validate: (v) => !!v &&
|
|
300
|
-
Array.isArray(v.results) &&
|
|
301
|
-
v.results.every((r) => !!r &&
|
|
302
|
-
typeof r.sessionId === "string" &&
|
|
303
|
-
Array.isArray(r.matches) &&
|
|
304
|
-
r.matches.every((m) => !!m && typeof m.snippet === "string")),
|
|
305
|
-
},
|
|
306
|
-
uploadAttachment: { kind: "invoke", validate: isAttachmentUploadReceipt },
|
|
307
|
-
listModels: { kind: "invoke", validate: isSeatModelCatalog },
|
|
308
|
-
onEvent: { kind: "event", validate: isLocalSessionEvent },
|
|
309
|
-
onToolPermissionRequest: { kind: "event", validate: isToolPermissionRequest },
|
|
514
|
+
start: { kind: "invoke", check: checkLocalSessionRecord },
|
|
515
|
+
sendMessage: { kind: "invoke", check: checkSendMessageAck },
|
|
516
|
+
interrupt: { kind: "invoke", check: checkInterruptAck },
|
|
517
|
+
stop: { kind: "invoke", check: checkStopAck },
|
|
518
|
+
respondToToolPermission: { kind: "invoke", check: checkOkAck },
|
|
519
|
+
cancelQueuedMessage: { kind: "invoke", check: checkBooleanAck },
|
|
520
|
+
reorderQueuedMessage: { kind: "invoke", check: checkBooleanAck },
|
|
521
|
+
promoteQueuedMessage: { kind: "invoke", check: checkBooleanAck },
|
|
522
|
+
setFocusedSession: { kind: "invoke", check: checkOkAck },
|
|
523
|
+
setModel: { kind: "invoke", check: checkBooleanAck },
|
|
524
|
+
getTranscript: { kind: "invoke", check: checkGetTranscriptResult },
|
|
525
|
+
searchSessions: { kind: "invoke", check: checkSessionSearchEnvelope },
|
|
526
|
+
uploadAttachment: { kind: "invoke", check: checkAttachmentUploadReceipt },
|
|
527
|
+
listModels: { kind: "invoke", check: checkSeatModelCatalog },
|
|
528
|
+
onEvent: { kind: "event", check: checkLocalSessionEvent },
|
|
529
|
+
onToolPermissionRequest: { kind: "event", check: checkToolPermissionRequest },
|
|
310
530
|
},
|
|
311
531
|
stores: {
|
|
312
|
-
sessionsState: {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
v.sessions.every((s) => isLocalSessionRecord(s)),
|
|
316
|
-
},
|
|
317
|
-
focusedSessionState: {
|
|
318
|
-
validate: (v) => {
|
|
319
|
-
const s = v;
|
|
320
|
-
return !!v && (s.sessionId === null || typeof s.sessionId === "string");
|
|
321
|
-
},
|
|
322
|
-
},
|
|
323
|
-
settingsState: {
|
|
324
|
-
validate: (v) => {
|
|
325
|
-
const s = v;
|
|
326
|
-
return (!!v &&
|
|
327
|
-
(s.defaultModel === null || typeof s.defaultModel === "string") &&
|
|
328
|
-
(typeof s.engineVersion === "undefined" || typeof s.engineVersion === "string") &&
|
|
329
|
-
(typeof s.dataRoot === "undefined" || typeof s.dataRoot === "string"));
|
|
330
|
-
},
|
|
331
|
-
},
|
|
532
|
+
sessionsState: { check: checkSessionsState },
|
|
533
|
+
focusedSessionState: { check: checkFocusedSessionState },
|
|
534
|
+
settingsState: { check: checkSettingsState },
|
|
332
535
|
},
|
|
333
536
|
};
|
|
334
537
|
/** verb 名清单 = `LOCAL_SESSIONS_SPEC.methods` 应有的键集(逐元素相等,门里断言)。 */
|
|
@@ -378,6 +581,8 @@ export const LOCAL_SESSION_EVENT_KEYS = [
|
|
|
378
581
|
"outcome",
|
|
379
582
|
"chrome",
|
|
380
583
|
];
|
|
584
|
+
// 🔴 REF-CC-067/070(BREAKING):`createdAt` → `createdAtMs`(epoch 单位入名);
|
|
585
|
+
// `boundCallId` + `boundInputHash` → `toctouBinding`(pair 一位承载)。三端消费坐标见 commit。
|
|
381
586
|
export const TOOL_PERMISSION_REQUEST_KEYS = [
|
|
382
587
|
"requestId",
|
|
383
588
|
"sessionId",
|
|
@@ -385,9 +590,8 @@ export const TOOL_PERMISSION_REQUEST_KEYS = [
|
|
|
385
590
|
"toolCallId",
|
|
386
591
|
"input",
|
|
387
592
|
"message",
|
|
388
|
-
"
|
|
389
|
-
"
|
|
390
|
-
"createdAt",
|
|
593
|
+
"toctouBinding",
|
|
594
|
+
"createdAtMs",
|
|
391
595
|
];
|
|
392
596
|
export const LOCAL_SESSION_RECORD_KEYS = [
|
|
393
597
|
"sessionId",
|
|
@@ -421,10 +625,193 @@ export const TOOL_PERMISSION_DECISION_KEYS = [
|
|
|
421
625
|
"remember",
|
|
422
626
|
];
|
|
423
627
|
export const START_SESSION_OPTION_KEYS = ["model", "title", "cwd", "excludeTools"];
|
|
628
|
+
/**
|
|
629
|
+
* 校验器覆盖表(REF-CC-068)—— 每个校验器**逐键**声明:哪些位真被校验、哪些位**刻意**不校验。
|
|
630
|
+
*
|
|
631
|
+
* 为什么需要它:`防再漂钉` 封的是「interface ↔ 名字常量 ↔ SPEC」两跳,**校验器这条链此前没人钉**。
|
|
632
|
+
* 给一个 interface 加一个必填位,门全绿,而新位从此不过校验(fail-closed 面出现缺口)。
|
|
633
|
+
* 正确做法不是「全键必查」—— 本文件有真正**刻意不查**的位(transcript 面直通),所以豁免必须显式登记
|
|
634
|
+
* 并带理由。门里对每个位投毒:说校验的必拒、说豁免的必过 ⇒ 这张表不可能变成一段过期的说明文字。
|
|
635
|
+
*
|
|
636
|
+
* 🔴 覆盖范围(wave2 回炉批 2026-08-02 补齐,首版只登了 6 个而发包说明写的是「每个校验器」——
|
|
637
|
+
* 那 8 个没登的恰好就是 REF-CC-072 本批新提名的那批 `check*`,实测「加一个必填位、全门照绿」在它们
|
|
638
|
+
* 身上仍然全开,即这张表自称堵死的失效形当时并没有被堵死):
|
|
639
|
+
* · **SPEC 面 15 个具名 `check`,14 个对象形在本表登记**,余下 1 个(裸布尔回执)逐形豁免;
|
|
640
|
+
* · 另加 4 个**嵌套**形(SeatModelInfo / SessionSearchResult / ToctouBinding /
|
|
641
|
+
* SeatChromeEnvelopeBase)—— 它们不是 SPEC 面的直接 check,但父校验器的覆盖表只登记得到
|
|
642
|
+
* `models` / `results` / `toctouBinding` / `chrome` 这一个顶层键,给**子形**加必填位同样全绿;
|
|
643
|
+
* · 合计 **18 条**。两个不适用的形逐形写在 `SEAT_VALIDATOR_COVERAGE_EXEMPT` 里。
|
|
644
|
+
* 完备性不靠这段话:门里逐条比对 `LOCAL_SESSIONS_SPEC` 每个 method/store 的 `check` **函数引用**
|
|
645
|
+
* 必须落在「本表 ∪ 豁免表」里 —— 再加一条 verb 而不登记,门当场红。
|
|
646
|
+
*
|
|
647
|
+
* `check` 位存的是**函数引用本身**(不是名字):名字查表要靠一张 guards 映射,而那张映射漏一行
|
|
648
|
+
* 就是又一个没人看见的缺口;引用相等是机械的。`guard` 位只在该形另有公开面 `is*` 谓词时登记,
|
|
649
|
+
* 门里额外校「谓词与 check 判决一致」。
|
|
650
|
+
*/
|
|
651
|
+
export const SEAT_VALIDATOR_KEY_COVERAGE = {
|
|
652
|
+
LocalSessionRecord: {
|
|
653
|
+
guard: "isLocalSessionRecord",
|
|
654
|
+
check: checkLocalSessionRecord,
|
|
655
|
+
validated: ["sessionId", "engineSessionId", "model", "title", "isRunning", "createdAt", "lastActivityAt", "queuedMessages"],
|
|
656
|
+
unchecked: [],
|
|
657
|
+
},
|
|
658
|
+
ToolPermissionRequest: {
|
|
659
|
+
guard: "isToolPermissionRequest",
|
|
660
|
+
check: checkToolPermissionRequest,
|
|
661
|
+
validated: ["requestId", "sessionId", "toolName", "toolCallId", "message", "toctouBinding", "createdAtMs"],
|
|
662
|
+
unchecked: [
|
|
663
|
+
{
|
|
664
|
+
key: "input",
|
|
665
|
+
why: "UNTRUSTED service-redacted 工具参数 / 问题载荷:形状由工具自己定(CC request-args 姿态),渲染端只读不回喂;缺席/畸形的渲染义务写在字段注释里。",
|
|
666
|
+
},
|
|
667
|
+
],
|
|
668
|
+
},
|
|
669
|
+
LocalSessionEvent: {
|
|
670
|
+
guard: "isLocalSessionEvent",
|
|
671
|
+
check: checkLocalSessionEvent,
|
|
672
|
+
validated: [
|
|
673
|
+
"type",
|
|
674
|
+
"sessionId",
|
|
675
|
+
"messages",
|
|
676
|
+
"error",
|
|
677
|
+
"errorCategory",
|
|
678
|
+
"code",
|
|
679
|
+
"request",
|
|
680
|
+
"data",
|
|
681
|
+
"sources",
|
|
682
|
+
"permissionMode",
|
|
683
|
+
"session",
|
|
684
|
+
"userMessageUuid",
|
|
685
|
+
"hasPreClearSession",
|
|
686
|
+
"requestId",
|
|
687
|
+
"outcome",
|
|
688
|
+
"chrome",
|
|
689
|
+
],
|
|
690
|
+
unchecked: [
|
|
691
|
+
{
|
|
692
|
+
key: "message",
|
|
693
|
+
why: "transcript 面直通位(CC fQe 逐字姿态:SDKMessage 体不做 schema 校验)—— 校它 = 引擎加一种消息体的那天整条转录被门吃掉。",
|
|
694
|
+
},
|
|
695
|
+
],
|
|
696
|
+
},
|
|
697
|
+
AttachmentUploadReceipt: {
|
|
698
|
+
guard: "isAttachmentUploadReceipt",
|
|
699
|
+
check: checkAttachmentUploadReceipt,
|
|
700
|
+
validated: ["id", "name", "mime", "sha256", "sizeBytes"],
|
|
701
|
+
unchecked: [],
|
|
702
|
+
},
|
|
703
|
+
SeatModelCatalog: {
|
|
704
|
+
guard: "isSeatModelCatalog",
|
|
705
|
+
check: checkSeatModelCatalog,
|
|
706
|
+
validated: ["models", "defaultId"],
|
|
707
|
+
unchecked: [],
|
|
708
|
+
},
|
|
709
|
+
SendMessageAck: {
|
|
710
|
+
guard: "isSendMessageAck",
|
|
711
|
+
check: checkSendMessageAck,
|
|
712
|
+
validated: ["sessionId", "userMessageUuid", "queued"],
|
|
713
|
+
unchecked: [],
|
|
714
|
+
},
|
|
715
|
+
// ── wave2 回炉批补齐:REF-CC-072 提名的 8 个 SPEC 面 check* ────────────────────────────────
|
|
716
|
+
InterruptAck: {
|
|
717
|
+
check: checkInterruptAck,
|
|
718
|
+
validated: ["interrupted"],
|
|
719
|
+
unchecked: [],
|
|
720
|
+
},
|
|
721
|
+
StopAck: {
|
|
722
|
+
check: checkStopAck,
|
|
723
|
+
validated: ["stopped"],
|
|
724
|
+
unchecked: [],
|
|
725
|
+
},
|
|
726
|
+
OkAck: {
|
|
727
|
+
check: checkOkAck,
|
|
728
|
+
validated: ["ok"],
|
|
729
|
+
unchecked: [],
|
|
730
|
+
},
|
|
731
|
+
GetTranscriptResult: {
|
|
732
|
+
check: checkGetTranscriptResult,
|
|
733
|
+
// `messages` 登在 validated:门校的是**数组形**这一层(与 `LocalSessionEvent.messages` 同口径)。
|
|
734
|
+
// 元素刻意不校(CC fQe 姿态,SDKMessage 体)—— 那是键**内**的粒度,覆盖表管的是键面,别把两者混谈。
|
|
735
|
+
validated: ["sessionId", "messages"],
|
|
736
|
+
unchecked: [],
|
|
737
|
+
},
|
|
738
|
+
SessionSearchEnvelope: {
|
|
739
|
+
check: checkSessionSearchEnvelope,
|
|
740
|
+
validated: ["results"],
|
|
741
|
+
unchecked: [],
|
|
742
|
+
},
|
|
743
|
+
SessionsState: {
|
|
744
|
+
check: checkSessionsState,
|
|
745
|
+
validated: ["sessions"],
|
|
746
|
+
unchecked: [],
|
|
747
|
+
},
|
|
748
|
+
FocusedSessionState: {
|
|
749
|
+
check: checkFocusedSessionState,
|
|
750
|
+
validated: ["sessionId"],
|
|
751
|
+
unchecked: [],
|
|
752
|
+
},
|
|
753
|
+
SettingsState: {
|
|
754
|
+
check: checkSettingsState,
|
|
755
|
+
validated: ["defaultModel", "engineVersion", "dataRoot"],
|
|
756
|
+
unchecked: [],
|
|
757
|
+
},
|
|
758
|
+
// ── wave2 回炉批补齐:父校验器只登记得到一个顶层键的 4 个嵌套形 ──────────────────────────
|
|
759
|
+
SeatModelInfo: {
|
|
760
|
+
check: (v) => checkSeatModelInfo(v, 0),
|
|
761
|
+
validated: ["id", "name", "provider", "contextWindow", "maxOutputTokens"],
|
|
762
|
+
unchecked: [],
|
|
763
|
+
},
|
|
764
|
+
SessionSearchResult: {
|
|
765
|
+
check: checkSessionSearchResult,
|
|
766
|
+
validated: ["sessionId", "title", "matches"],
|
|
767
|
+
unchecked: [],
|
|
768
|
+
},
|
|
769
|
+
ToctouBinding: {
|
|
770
|
+
check: checkToctouBinding,
|
|
771
|
+
validated: ["callId", "inputHash"],
|
|
772
|
+
unchecked: [],
|
|
773
|
+
},
|
|
774
|
+
SeatChromeEnvelopeBase: {
|
|
775
|
+
check: (v) => checkSeatChromeEnvelope(v, "chrome"),
|
|
776
|
+
validated: ["kind", "laneProof"],
|
|
777
|
+
unchecked: [],
|
|
778
|
+
},
|
|
779
|
+
};
|
|
780
|
+
/**
|
|
781
|
+
* 逐形豁免(REF-CC-068 的另一半):**不适用**覆盖表的具名校验器 —— 每条写清为什么,以及它的判别力
|
|
782
|
+
* 由谁承担。这张表存在的唯一理由是让「本表 ∪ 覆盖表 ⊇ SPEC 面全部 check」这条完备性钉能机械成立:
|
|
783
|
+
* 没有它,「某个 check 没登记」和「某个 check 不该登记」在门里长得一模一样。
|
|
784
|
+
*/
|
|
785
|
+
export const SEAT_VALIDATOR_COVERAGE_EXEMPT = [
|
|
786
|
+
{
|
|
787
|
+
label: "BooleanAck",
|
|
788
|
+
check: checkBooleanAck,
|
|
789
|
+
why: "载荷是**裸布尔**(queue 三件 + setModel 的 CC 形回执,false = too-late/no-op),没有键面 ⇒ `keyof` 覆盖表在它身上无从谈起。判别力由 J 段承担:认 true/false、拒非布尔(对象 / 字符串 'true' / null / undefined 四形逐一投毒)。",
|
|
790
|
+
},
|
|
791
|
+
{
|
|
792
|
+
label: "LaneProof",
|
|
793
|
+
check: (v) => checkLaneProof(v, "laneProof"),
|
|
794
|
+
why: "判别式联合:必填键面随 `lane` 臂而变(subagent 要 parentToolCallId、workflow 要 via+key),而覆盖表的方法是「一个最小合法体 + 逐键投毒」—— 那套方法在它身上没有判别力(main 臂对另外两臂的伴随位本来就不该拒,投毒过了不代表漏校)。判别力由 `LANE_PROOF_LANE_TUPLE` ⇄ seam.ts 车道联合的双向 `Covers` + F 段逐臂投毒(空对象 / 非对象 / 未登记车道词)承担。",
|
|
795
|
+
},
|
|
796
|
+
];
|
|
797
|
+
/** 双向覆盖钉:`Exclude` 两个方向都必须塌成 `never`,少一个键红、多一个键也红。
|
|
798
|
+
* `Covers`(REF-CC-145/dup-12,P3 wave1 卡C-argv)改吃共用叶 typePins.ts——四份逐字重复钉之一。 */
|
|
424
799
|
const _seatContractPins = [
|
|
425
800
|
true, true, true, true, true, true, true, true, true,
|
|
426
801
|
true, true, true, true, true, true, true, true,
|
|
427
802
|
true, true,
|
|
428
803
|
true, true, true, true,
|
|
804
|
+
true, true, true, true,
|
|
805
|
+
true, true,
|
|
806
|
+
true, true,
|
|
807
|
+
true, true,
|
|
808
|
+
true, true,
|
|
809
|
+
true, true, true, true,
|
|
810
|
+
true, true, true, true, true, true,
|
|
811
|
+
true, true, true, true, true, true, true, true, true, true, true, true,
|
|
812
|
+
// wave2 回炉批:覆盖表补齐的 12 个形 × 双向 = 24
|
|
813
|
+
true, true, true, true, true, true, true, true,
|
|
814
|
+
true, true, true, true, true, true, true, true,
|
|
815
|
+
true, true, true, true, true, true, true, true,
|
|
429
816
|
];
|
|
430
817
|
void _seatContractPins;
|