@sema-agent/client-core 0.13.0 → 0.15.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 +3 -1
- package/dist/adapt/arms.d.ts +10 -1
- package/dist/adapt/arms.js +12 -2
- package/dist/adapt/panelTasks.d.ts +6 -1
- package/dist/adapt/textStream.d.ts +15 -1
- package/dist/adapt/turnFlags.d.ts +5 -1
- package/dist/adapt/wireShapes.d.ts +4 -1
- package/dist/adapt.d.ts +1 -1
- package/dist/adapt.js +7 -2
- package/dist/adapter/downstream/eventToSdkMessage.d.ts +4 -2
- package/dist/adapter/downstream/eventToSdkMessage.js +9 -1
- package/dist/adapter/downstream/terminalToSdkResult.js +4 -2
- package/dist/adapter/runStream.d.ts +8 -1
- package/dist/adapter/runStream.js +70 -4
- package/dist/adapter/types.d.ts +26 -0
- package/dist/hitl/approvalsFeed.d.ts +22 -1
- package/dist/hitl/approvalsFeed.js +76 -5
- package/dist/hitl/frameRouter.js +49 -8
- package/dist/hitl/gateLedger.d.ts +13 -1
- package/dist/hitl/gateLedger.js +2 -2
- package/dist/hitl/parkResolver.js +10 -1
- package/dist/hitl/planReviewWire.js +48 -20
- package/dist/hitl/toolApprovalWire.js +11 -1
- package/dist/hooksWireCaps.js +10 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +13 -0
- package/dist/liveQuestionStore.d.ts +13 -0
- package/dist/liveQuestionStore.js +15 -0
- package/dist/model/catalogLoader.d.ts +170 -0
- package/dist/model/catalogLoader.js +382 -0
- package/dist/model/providerAuth.d.ts +155 -0
- package/dist/model/providerAuth.js +190 -0
- package/dist/model/providerPresets.d.ts +16 -0
- package/dist/notifications.d.ts +15 -1
- package/dist/notifications.js +90 -10
- package/dist/printToolResultFrame.d.ts +12 -4
- package/dist/printToolResultFrame.js +11 -21
- package/dist/unrefTimer.d.ts +14 -3
- package/package.json +1 -1
package/dist/hitl/frameRouter.js
CHANGED
|
@@ -26,11 +26,17 @@ export function isAskTool(toolName) {
|
|
|
26
26
|
return isAskToolLoose(toolName);
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
*
|
|
29
|
+
* 「这个**工具名**归 gate 管」的判据(#110 修,2026-08-02)。
|
|
30
30
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* ⚠️ 它只是 park 判定的**名字腿**对位件,不是全部 —— park 判定 `isToolApprovalGate` 还有一条
|
|
32
|
+
* 一等 kind 腿(`gate.kind==='tool_approval'`,放行任意 toolName),名字面上没有对位物。
|
|
33
|
+
* tool_end 手柄用的是下面的 `isGatedToolEnd`(名字腿 ∪ abort 标记腿),两者的集合关系与理由
|
|
34
|
+
* 写在那个函数的头注里([2393] hitl-F1)。本函数还被 `routeToolStart` 用来决定「要不要记 args /
|
|
35
|
+
* 要不要跟 `lastFsOrShellGatedCallId`」,那两件事本来就只对名字识别得出的族成立。
|
|
36
|
+
*
|
|
37
|
+
* 🔴 它必须与 park 判定的**名字腿**(`isAskTool` / `toolNameIsFsWrite` / `toolNameIsShellExec`)
|
|
38
|
+
* 覆盖同一个集合 —— 两边不同集就是 [paired-mechanisms-must-share-premise] 那种「park 认得出、
|
|
39
|
+
* HOLD 认不出」的半场病:[2150] S1 给 park 判定加了 `toolNameIsShellExec`(Bash/shell 类进 gate 了),但
|
|
34
40
|
* tool_end 手柄的 HOLD/REJECT 谓词还停在 `isAskTool || toolNameIsFsWrite` ——
|
|
35
41
|
* 于是 Bash 的 durable gate 走到这里就两件事同时出错:
|
|
36
42
|
* ① park 期的毒化帧(`tool_end{isError:true, output:"Operation aborted"}`)没被 HOLD,
|
|
@@ -47,6 +53,36 @@ function isGatedToolName(name) {
|
|
|
47
53
|
return false;
|
|
48
54
|
return toolNameIsFsWrite(name) || toolNameIsShellExec(name);
|
|
49
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* 「这一帧 tool_end 归 gate 管」的判据 —— 上面那条**名字腿**加一条 kind 腿的替身([2393] hitl-F1,
|
|
58
|
+
* 2026-08-02)。
|
|
59
|
+
*
|
|
60
|
+
* 🔴 为什么名字腿一条不够(上面那句「必须覆盖同一个集合」在 kind 腿上的反例):park 判定
|
|
61
|
+
* `isToolApprovalGate` 有**两条**腿 —— 名字族(ask / fs 写三件 / Bash)与**一等 kind**
|
|
62
|
+
* (`gate.kind === 'tool_approval'`,`toolApprovalWire.isFsApprovalGate` 首行)。后者放行**任意**
|
|
63
|
+
* toolName:server 对名单外的工具出一等 kind 审批 park 时,park 侧认、tool_end 侧的名字腿认不出,
|
|
64
|
+
* 于是 #110 那三件事在这类 gate 上原样重演一遍(毒化帧上屏 / `markEnded` 吃掉重放的真结果 /
|
|
65
|
+
* deny 的 CC 文案进不去)。
|
|
66
|
+
*
|
|
67
|
+
* 🔴 为什么不能在名字腿里补 kind:`tool_end` 帧上**根本没有 kind 位** —— gate 的 kind 只在
|
|
68
|
+
* `done{suspended}.checkpointGate` / `suspended.gate` 上,而那两帧**晚于**毒化 tool_end 到达
|
|
69
|
+
* (park 帧序:tool_start → tool_end(毒化)→ done{suspended})。「park 时按 callId 打标、
|
|
70
|
+
* tool_end 时读台账」这条路对**这一帧**来得太晚。帧面上唯一可读的信号是引擎铸的**确切 abort
|
|
71
|
+
* 标记** `ENGINE_ABORT_TOOL_RESULT`(见下方 ④ 臂头注:core 对被 gate/连坐 abort 的 call 恒铸
|
|
72
|
+
* 这一串),所以本判据 = 名字腿 ∪ abort 标记腿。
|
|
73
|
+
*
|
|
74
|
+
* 🔴 集合关系(说清楚,别让下一棒再以为两边逐字相等):本判据是 park 判定的**超集**(它还会盖到
|
|
75
|
+
* 被同 turn 连坐 abort 的非 gate call)。方向是有意的,且不对称成立 —— 多盖一帧的代价是那一帧
|
|
76
|
+
* **晚**上屏(HOLD 的帧在模型推进 / 终帧 / fail-soft 三处必被 flush,一帧都不会丢),而少盖一帧
|
|
77
|
+
* 的代价是用户按了 Yes 却只看到 `Error: Operation aborted`。顺带把连坐 abort 的 call 从
|
|
78
|
+
* `markEnded` 里救出来:它们本来也会在 resume 后被模型重发,而旧码已把它们记成已收口。
|
|
79
|
+
* ⚠️ 普通工具错(非 abort 标记)照旧**不进** HOLD —— [2084]①-b 的收窄没有被放宽,负控 F1-c 钉着。
|
|
80
|
+
*/
|
|
81
|
+
function isGatedToolEnd(ev) {
|
|
82
|
+
if (isGatedToolName(ev.toolName))
|
|
83
|
+
return true;
|
|
84
|
+
return ev.output === ENGINE_ABORT_TOOL_RESULT;
|
|
85
|
+
}
|
|
50
86
|
/** done 帧的 AskUserQuestion park 形状(结构性读;别的终态一律 false)。 */
|
|
51
87
|
function isAskGatePark(result) {
|
|
52
88
|
const r = result;
|
|
@@ -128,9 +164,11 @@ const TOOL_END_ARMS = [
|
|
|
128
164
|
{
|
|
129
165
|
// ② 同步帧腿 deny 且 callId 没关联上:下一张 gated 报错帧即该 gate 的收口帧,stamp REJECT 文案。
|
|
130
166
|
// #110:stamp 也要盖 shell —— 否则按 No 时用户看到的是引擎原文 `Operation aborted`。
|
|
167
|
+
// [2393] hitl-F1:改用 `isGatedToolEnd`(名字腿 ∪ abort 标记腿)—— 一等 kind gate 的
|
|
168
|
+
// 收口帧名字腿认不出,不盖它 = 用户按了 No 却看到引擎原文。
|
|
131
169
|
id: 'deny-stamp-next',
|
|
132
170
|
run(ev, callId, led) {
|
|
133
|
-
if (ev.isError !== true || !
|
|
171
|
+
if (ev.isError !== true || !isGatedToolEnd(ev) || led.isDenied(callId))
|
|
134
172
|
return undefined;
|
|
135
173
|
if (!led.takeDenyStamp())
|
|
136
174
|
return undefined;
|
|
@@ -143,7 +181,7 @@ const TOOL_END_ARMS = [
|
|
|
143
181
|
// ③ 三选卡 No:重放的报错帧 stamp REJECT 文案 → vendored 卡渲 `User rejected <op> to <path>`。
|
|
144
182
|
id: 'denied-call',
|
|
145
183
|
run(ev, callId, led) {
|
|
146
|
-
if (ev.isError !== true || !
|
|
184
|
+
if (ev.isError !== true || !isGatedToolEnd(ev))
|
|
147
185
|
return undefined;
|
|
148
186
|
if (!led.takeDenied(callId))
|
|
149
187
|
return undefined;
|
|
@@ -160,7 +198,7 @@ const TOOL_END_ARMS = [
|
|
|
160
198
|
// —— 收窄后它们零滞后直达转录(落到 ⑤ 通用收口),不再依赖「推进信号放行」。
|
|
161
199
|
id: 'hold-poison',
|
|
162
200
|
run(ev, callId, led) {
|
|
163
|
-
if (ev.isError !== true || !
|
|
201
|
+
if (ev.isError !== true || !isGatedToolEnd(ev))
|
|
164
202
|
return undefined;
|
|
165
203
|
if (ev.output !== ENGINE_ABORT_TOOL_RESULT)
|
|
166
204
|
return undefined;
|
|
@@ -288,7 +326,10 @@ function routeSuspended(ev, ctx) {
|
|
|
288
326
|
ctx.taskId.current) {
|
|
289
327
|
return { kind: 'park', gate: 'fs' };
|
|
290
328
|
}
|
|
291
|
-
|
|
329
|
+
// 其余 gate:透传。⚠️ ADAPTER-F8 注纠(2026-08-02):原文写「eventToSdkMessage 出 null」——
|
|
330
|
+
// 那个返回形已随 REF-CC-058 退役,今天它出的是 `EventProjection` 三态,永不是 null;本臂的
|
|
331
|
+
// 帧走到投影器时落 `none/hitl_out_of_slice`(HITL 登记是 run driver 的活,不在投影切片内)。
|
|
332
|
+
return { kind: 'yield', events: [ev] };
|
|
292
333
|
}
|
|
293
334
|
function routeDone(ev, ctx) {
|
|
294
335
|
const { led } = ctx;
|
|
@@ -99,7 +99,19 @@ export interface GateLedger {
|
|
|
99
99
|
* 「已解决」最多多读一轮、被 hop 预算兜底;误判成「真失败」烧会话)。
|
|
100
100
|
*/
|
|
101
101
|
markDecided(callId: string): void;
|
|
102
|
-
|
|
102
|
+
/**
|
|
103
|
+
* 一次性消费([2393] hitl-F2,2026-08-02):记号被一次 park 失败消费掉就**失效**。
|
|
104
|
+
*
|
|
105
|
+
* 🔴 为什么必须一次性:这条判据的正当性完全来自「durable re-attach 会把**那一次**已决断的 park
|
|
106
|
+
* 重放一遍」——重放窗口只有一次。而它的两个输入都是单调的:`lastFsOrShellGatedCallId()` 不随
|
|
107
|
+
* tool_end 清空(见上方声明处注,那是**故意**的,park 重放时没有新的 tool_start 可跟),
|
|
108
|
+
* `decidedGates` 又只增不删 ⇒ 只要这个 turn 里成功决断过一次,**之后每一次** park 失败都会命中
|
|
109
|
+
* 身份匹配,不论真因是什么。实测后果:`approvals.list` 网络失败这类真失败被连续 24 次判成
|
|
110
|
+
* 「已解决」,吃满 `MAX_GATE_HOPS` 才吐一句 `gate hop limit exceeded` —— 用户白等 24 轮往返,
|
|
111
|
+
* 拿到的还是一句与真因无关的话(真因被预算话术顶掉了)。
|
|
112
|
+
* 消费一次即失效之后:第一次(= 真的重放)照旧救回,第二次就是诚实的 fail-soft。
|
|
113
|
+
*/
|
|
114
|
+
takeDecided(callId: string): boolean;
|
|
103
115
|
/** 只给 debug 串用的规模位(`[decided so far: N]`)。 */
|
|
104
116
|
decidedCount(): number;
|
|
105
117
|
}
|
package/dist/hitl/gateLedger.js
CHANGED
|
@@ -103,8 +103,8 @@ export function createGateLedger() {
|
|
|
103
103
|
markDecided(callId) {
|
|
104
104
|
decidedGates.add(callId);
|
|
105
105
|
},
|
|
106
|
-
|
|
107
|
-
return decidedGates.
|
|
106
|
+
takeDecided(callId) {
|
|
107
|
+
return decidedGates.delete(callId);
|
|
108
108
|
},
|
|
109
109
|
decidedCount() {
|
|
110
110
|
return decidedGates.size;
|
|
@@ -200,7 +200,16 @@ export async function resolvePark(park, ctx) {
|
|
|
200
200
|
// `FsApprovalOutcome` 今天没有 `code`(toolApprovalWire.ts,C-bridge 域),失败原因若不是
|
|
201
201
|
// 那三个文案子串(例如 `approvals.list` 自身网络失败)就会漏判。已决断身份匹配是独立于
|
|
202
202
|
// 文案的第二判据 —— 两臂任一命中都按「已解决」处置,方向偏宽(见台账 `markDecided` 处注)。
|
|
203
|
-
|
|
203
|
+
//
|
|
204
|
+
// [2393] hitl-F2(2026-08-02):这条身份判据**一次性消费**(`takeDecided` 而不是 `isDecided`)。
|
|
205
|
+
// 它的正当性只覆盖「durable re-attach 把**那一次**已决断的 park 重放一遍」这一个窗口,而它的两个
|
|
206
|
+
// 输入都是单调的(`lastFsOrShellGatedCallId()` 故意不随 tool_end 清空 + `decidedGates` 只增),
|
|
207
|
+
// 不消费就等于:本 turn 只要成功决断过一次,之后**每一次** park 失败都被判「已解决」——
|
|
208
|
+
// `approvals.list` 网络失败这类真失败会连吃 24 个 hop,最后吐一句与真因无关的 `gate hop limit`。
|
|
209
|
+
// 消费点写在 `outcome.kind === 'failed'` 之内:决断成功的那一轮压根不该动这个记号。
|
|
210
|
+
const alreadyDecidedById = outcome.kind === 'failed' &&
|
|
211
|
+
candidateGatedCallId !== undefined &&
|
|
212
|
+
led.takeDecided(candidateGatedCallId);
|
|
204
213
|
if (outcome.kind === 'failed' && (isAlreadyResolvedFailure(outcome) || alreadyDecidedById)) {
|
|
205
214
|
const seq = led.lastSeq();
|
|
206
215
|
hostLog('debug', `liveHitlAskWire: gate already resolved (${outcome.reason}) — replayed park, re-attaching runs.events(${taskId})${seq ? ` from seq ${seq}` : ''} instead of failing the turn` +
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* 挂账(诚实边界):approve 后壳本地的 plan-mode footer 徽章不自动退(CC 会退)——退 mode 的
|
|
37
37
|
* app-state seam 不在本模块可达面,记 rc.47 接 REPL 层;engine 侧 handsReadOnly 已由 resume 处理。
|
|
38
38
|
*/
|
|
39
|
-
import { publishQuestionFrame, registerLocalQuestionResponder, } from '../liveQuestionStore.js';
|
|
39
|
+
import { publishQuestionFrame, registerLocalQuestionResponder, hasLocalQuestionResponder, } from '../liveQuestionStore.js';
|
|
40
40
|
import { hostLog } from '../host.js';
|
|
41
41
|
import { makeEngineWireClient } from '../engineWireSdk.js';
|
|
42
42
|
import { engineWireTarget } from '../engineWireTarget.js';
|
|
@@ -92,6 +92,28 @@ const armedPlanReviewTaskIds = new Set();
|
|
|
92
92
|
export function _resetArmedPlanReviewsForTest() {
|
|
93
93
|
armedPlanReviewTaskIds.clear();
|
|
94
94
|
}
|
|
95
|
+
/** 合成 questionId 的唯一铸口(短路臂与首次 arm 共用,别各铸各的)。 */
|
|
96
|
+
function planReviewQuestionId(taskId) {
|
|
97
|
+
return `plan-review:${taskId}`;
|
|
98
|
+
}
|
|
99
|
+
/** 审批卡的题面 —— 首次 arm 与「重放 arm 重新弹卡」共用同一份,两处不许各写一份。 */
|
|
100
|
+
function publishPlanReviewCard(taskId) {
|
|
101
|
+
publishQuestionFrame({
|
|
102
|
+
type: 'question',
|
|
103
|
+
questionId: planReviewQuestionId(taskId),
|
|
104
|
+
questions: [
|
|
105
|
+
{
|
|
106
|
+
header: 'Plan review',
|
|
107
|
+
question: 'The plan is ready for review. Approve it and start the implementation?',
|
|
108
|
+
options: [
|
|
109
|
+
{ label: APPROVE_LABEL, description: 'Resume the task now and execute the plan (runs to completion engine-side)' },
|
|
110
|
+
{ label: REJECT_LABEL, description: 'Discard this plan and stay in plan mode to refine it' },
|
|
111
|
+
],
|
|
112
|
+
multiSelect: false,
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
});
|
|
116
|
+
}
|
|
95
117
|
/**
|
|
96
118
|
* Arm the approval card for a parked plan_review (call with the RAW done.result). Returns true when
|
|
97
119
|
* armed (live path + shape matched, INCLUDING the idempotent "already armed" short-circuit — see
|
|
@@ -107,13 +129,33 @@ export function armPlanReviewApproval(result) {
|
|
|
107
129
|
return false;
|
|
108
130
|
const { taskId } = result;
|
|
109
131
|
if (armedPlanReviewTaskIds.has(taskId)) {
|
|
110
|
-
// REF-CC-027:同一张卡还没消解就被重放的 arm
|
|
111
|
-
|
|
112
|
-
|
|
132
|
+
// REF-CC-027:同一张卡还没消解就被重放的 arm 撞上——幂等,**不二次注册** responder。
|
|
133
|
+
//
|
|
134
|
+
// [2393] hitl-F3(2026-08-02):但「不二次注册」不等于「什么都不做」。旧码在这里只打一行
|
|
135
|
+
// debug 就 `return true`,于是这条短路成了一堵挡死重开路径的墙:
|
|
136
|
+
// · `liveQuestionStore.ts` 的契约明写「dialog dismissed without answering ⇒ caller cleans
|
|
137
|
+
// up」,而本模块**没有** dismiss/abort 腿 —— 用户 Esc 关掉卡(不作答)之后,武装态
|
|
138
|
+
// 一直是 true,responder 也一直挂着;
|
|
139
|
+
// · 此后每一次 arm(durable re-attach 重放 / 409 撞上再开)都走这条短路:**一张卡都不弹**,
|
|
140
|
+
// 调用方拿到 true 于是也不渲终帧 —— 用户面静默挂住,plan 卡在 park 里没有任何出口。
|
|
141
|
+
// 修法按「还在不在」分两支,两支都不产生第二次 `registerLocalQuestionResponder`:
|
|
142
|
+
if (hasLocalQuestionResponder(planReviewQuestionId(taskId))) {
|
|
143
|
+
// responder 还绑着 ⇒ 这张卡仍然可作答,只是可能已经不在屏上了。**重新弹一次**
|
|
144
|
+
// (同一个 questionId、同一份题面、同一个 responder):卡还开着的宿主只是收到一帧同形
|
|
145
|
+
// 的重绘,卡已经被 dismiss 的宿主则拿回了它的重开路径。
|
|
146
|
+
hostLog('debug', `planReviewWire: arm replayed — re-presenting the still-armed card for task ${taskId}`);
|
|
147
|
+
publishPlanReviewCard(taskId);
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
// responder 不在了(被别的注册顶掉后注销 / 被单方面清理)⇒ 武装态是**陈旧**的。
|
|
151
|
+
// 复用它 = 弹一张没有任何人能收答的卡(比不弹更坏:它看起来能点)。丢掉陈旧记号,
|
|
152
|
+
// 往下走整条重新 arm —— 这正是 REF-CC-027 头注里那个「悬空引用」形的收尾。
|
|
153
|
+
hostLog('debug', `planReviewWire: stale armed state for task ${taskId} (no local responder) — re-arming from scratch`);
|
|
154
|
+
armedPlanReviewTaskIds.delete(taskId);
|
|
113
155
|
}
|
|
114
156
|
armedPlanReviewTaskIds.add(taskId);
|
|
115
157
|
armedTaskId = taskId;
|
|
116
|
-
const questionId =
|
|
158
|
+
const questionId = planReviewQuestionId(taskId);
|
|
117
159
|
const unregister = registerLocalQuestionResponder(questionId, async (_id, answer) => {
|
|
118
160
|
// REF-CC-027:一次性 —— 首句就注销 + 清武装态(照 askGateWire 的 cleanup() 形)。
|
|
119
161
|
// liveQuestionStore.respondToQuestion 自身也是一次性取件(delete-before-invoke),这里是
|
|
@@ -130,21 +172,7 @@ export function armPlanReviewApproval(result) {
|
|
|
130
172
|
void decidePlanReview(taskId, decided);
|
|
131
173
|
return { ok: true };
|
|
132
174
|
});
|
|
133
|
-
|
|
134
|
-
type: 'question',
|
|
135
|
-
questionId,
|
|
136
|
-
questions: [
|
|
137
|
-
{
|
|
138
|
-
header: 'Plan review',
|
|
139
|
-
question: 'The plan is ready for review. Approve it and start the implementation?',
|
|
140
|
-
options: [
|
|
141
|
-
{ label: APPROVE_LABEL, description: 'Resume the task now and execute the plan (runs to completion engine-side)' },
|
|
142
|
-
{ label: REJECT_LABEL, description: 'Discard this plan and stay in plan mode to refine it' },
|
|
143
|
-
],
|
|
144
|
-
multiSelect: false,
|
|
145
|
-
},
|
|
146
|
-
],
|
|
147
|
-
});
|
|
175
|
+
publishPlanReviewCard(taskId);
|
|
148
176
|
hostLog('debug', `planReviewWire: approval card armed for parked plan (task ${taskId})`);
|
|
149
177
|
return true;
|
|
150
178
|
}
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
* 都返回 failed —— 调用方回退「flush + 原样终帧」的诚实红,绝不更糟。
|
|
62
62
|
* 🔴 UNTRUSTED:gate args 为模型作文(service 已 redact),只渲染绝不回喂;决断只带 decision 枚举。
|
|
63
63
|
*/
|
|
64
|
-
import { HitlBridge, findPendingForTask } from './hitlBridge.js';
|
|
64
|
+
import { HitlBridge, HitlSafetyError, findPendingForTask } from './hitlBridge.js';
|
|
65
65
|
import { hostLog } from '../host.js';
|
|
66
66
|
import { createSessionSlot, DEFAULT_SESSION_KEY } from '../sessionSlot.js';
|
|
67
67
|
import { readEngineActiveBgTasks } from '../fleet/fleetLedger.js';
|
|
@@ -256,6 +256,16 @@ export async function surfaceFsApprovalAndDecide(deps, taskId, argsByCall, signa
|
|
|
256
256
|
return { kind: 'decided', gatedCallId };
|
|
257
257
|
}
|
|
258
258
|
catch (e) {
|
|
259
|
+
// 🔴 [2393] hitl-F4(2026-08-02):回退臂**只**兜「老 server 不识别 remember ⇒ 400 未知键」
|
|
260
|
+
// 这一形。`HitlSafetyError` 是 hitlBridge 的**安全信号**闭集(hitlBridge.ts:9 / decideTool
|
|
261
|
+
// 头注末句逐字:「A binding mismatch (409) is re-raised as a `HitlSafetyError`
|
|
262
|
+
// ('binding_mismatch') — the caller re-presents, NEVER auto-retries」),旧的 catch-all
|
|
263
|
+
// 把它一并吞了,然后**立刻自动重发**一次纯 approve —— 正是那条铁律禁的动作:
|
|
264
|
+
// binding 不匹配意味着「人看见的那一行在他决断期间被换掉了」,自动重试等于替人对一件
|
|
265
|
+
// 他没看过的事按了 Yes。`no_pending` 同族(那一行已经没了,重试同样只会再失败一次)。
|
|
266
|
+
// 上抛给外层 catch ⇒ typed `failed` ⇒ 调用方走 fail-soft 诚实红,由人重新决断。
|
|
267
|
+
if (e instanceof HitlSafetyError)
|
|
268
|
+
throw e;
|
|
259
269
|
hostLog('debug', `liveToolApprovalWire: decide(approve+remember) failed (${String(e)}) — falling back to plain approve`);
|
|
260
270
|
}
|
|
261
271
|
}
|
package/dist/hooksWireCaps.js
CHANGED
|
@@ -98,14 +98,22 @@ export function hooksForWire() {
|
|
|
98
98
|
return undefined;
|
|
99
99
|
// trust gate first (cheapest + broadest): untrusted interactive workspace ⇒ no hooks anywhere,
|
|
100
100
|
// the engine leg included — same invariant the local executor enforces per-fire.
|
|
101
|
+
// [2393] F-2(fix-outright):此前这条 catch 是 **fail-OPEN**(fall through),理由句写「端侧本地
|
|
102
|
+
// 执行器仍 per-fire 把门」—— 那句在本包语境下是**假话**,本文件头第三段自己写明:壳里那条
|
|
103
|
+
// catch 的前提是单进程本地执行器,**库里没有那个兜底**。后果是「未受信工作区的 hooks 绝不上
|
|
104
|
+
// wire」这条被文件头称作安全门的不变量,在 SettingsPort 抛错时整个失效(用户/项目 settings 里的
|
|
105
|
+
// `command` 型 hook 照投给引擎真执行)。而同一函数下方对**同类失败**(port 抛)的 policy 读已选
|
|
106
|
+
// fail-closed 并写明「治理策略读不出来时更安全的默认应是当作有限制」—— 两道门相邻、同类输入,
|
|
107
|
+
// 结论必须一致,更敏感的那道不许留在宽的一侧。改成 fail-closed:信任查不出来 = 当作未受信。
|
|
101
108
|
try {
|
|
102
109
|
if (settings.shouldSkipHookDueToTrust()) {
|
|
103
110
|
hostLog('debug', 'hooksWireCaps: workspace not trusted — no hooks projected to the engine');
|
|
104
111
|
return undefined;
|
|
105
112
|
}
|
|
106
113
|
}
|
|
107
|
-
catch {
|
|
108
|
-
|
|
114
|
+
catch (err) {
|
|
115
|
+
hostLog('debug', `hooksWireCaps: workspace trust check threw — fail-closed (no hooks projected to the engine): ${String(err)}`);
|
|
116
|
+
return undefined;
|
|
109
117
|
}
|
|
110
118
|
let policy = null;
|
|
111
119
|
// REF-CC-155(midband-03,fix-outright,行为面已裁 GO — 安全敏感面 [2374] 背书):policy 是
|
package/dist/index.d.ts
CHANGED
|
@@ -223,5 +223,7 @@ export * from './seatContract.js';
|
|
|
223
223
|
export * from './agentSession/contract.js';
|
|
224
224
|
export * from './agentSession/backgroundView.js';
|
|
225
225
|
export * from './model/catalog.js';
|
|
226
|
+
export * from './model/catalogLoader.js';
|
|
227
|
+
export * from './model/providerAuth.js';
|
|
226
228
|
export * from './websearch/searchProviderPresets.js';
|
|
227
229
|
export * from './env/localeGeo.js';
|
package/dist/index.js
CHANGED
|
@@ -324,6 +324,19 @@ export * from './agentSession/backgroundView.js';
|
|
|
324
324
|
// 🔴 绝不半解析:schemaVersion 超区间/任一行形状坏 ⇒ 整份弃用走兜底(端渲「内置版本(离线)」
|
|
325
325
|
// 靠 `source` + `online.reason`,所以「静默用兜底」在本层是可观测的)。
|
|
326
326
|
export * from './model/catalog.js';
|
|
327
|
+
// ── design/166(2026-08-04):目录客户端消费链 —— 候选链 loader + 供应商凭证双轨 seam ──────────
|
|
328
|
+
// 🔴 `model/catalogLoader.ts` 是 catalog.ts 的**宿主面**,不是第二个解析器:候选链遍历 +
|
|
329
|
+
// 传输硬门(https + 域白名单,**重定向目标过同一道门**)+ `catalog.sha256` 旁签 + 缓存信封,
|
|
330
|
+
// 载荷判决(schemaVersion/逐行形状/整份弃)全部委托 `resolveModelCatalog`,一行不重写。
|
|
331
|
+
// 🔴 缓存**落盘**走注入口 `CatalogCachePort`(本包 index 闭包零 Node 内建是机械门守着的硬法;
|
|
332
|
+
// tmp+rename 的原子性是实现方的契约,参考实现在 catalogLoader.ts 的类型注释里)。
|
|
333
|
+
export * from './model/catalogLoader.js';
|
|
334
|
+
// 🔴 `model/providerAuth.ts` 与 registry SSO(cloudAuth)**物理分文件、零共享代码**
|
|
335
|
+
// ([reuse-single-sso-no-parallel-auth]:它是模型供应商凭证轨,不是 sema 登录轨)。
|
|
336
|
+
// `DEVICE_AUTH_PROVIDERS` 今天是**空表**(宁空勿假:没有我们自己名下的 client_id 之前,
|
|
337
|
+
// 填别的编辑器插件的 app id = 冒用,与 §6 判 C 档出局同一条判据),因此
|
|
338
|
+
// `supportsDeviceCodeAuth` 对每一家如实返回 false、UI 不显示该选项 —— 注册一家 = 加一行。
|
|
339
|
+
export * from './model/providerAuth.js';
|
|
327
340
|
// ── 搜索 provider 目录 + 地域预选(2026-07-31)──────────────────────────────────────────────────
|
|
328
341
|
// `websearch/searchProviderPresets` = model 目录的**同形不同表**姊妹件:数据在 json、类型与查询
|
|
329
342
|
// 在 ts。它编译出来的不是模型目录而是**引擎部署 env**(`WEB_SEARCH_*`)。
|
|
@@ -68,6 +68,19 @@ type RespondFn = (id: string, answer: QuestionAnswer, opts?: {
|
|
|
68
68
|
/** Register a one-shot local responder for a SYNTHETIC question frame (e.g. `plan-review:<taskId>`).
|
|
69
69
|
* Returns an unregister fn (dialog dismissed without answering ⇒ caller cleans up). */
|
|
70
70
|
export declare function registerLocalQuestionResponder(id: string, fn: RespondFn): () => void;
|
|
71
|
+
/**
|
|
72
|
+
* Is a local responder for this synthetic questionId still bound?([2393] hitl-F3,2026-08-02)
|
|
73
|
+
*
|
|
74
|
+
* 🔴 为什么这是**只读探询**而不是「谁注册谁自己记着就行」:上面那条契约把「对话框被 dismiss 却没
|
|
75
|
+
* 作答」的清理责任交给了 caller,而 caller(planReviewWire)自己那份武装态与这张表是**两个**容器 ——
|
|
76
|
+
* 两个容器各自都可能被单方面改动(`respondToQuestion` 的一次性 delete、别的注册把同 id 顶掉后再
|
|
77
|
+
* 注销),于是「我以为还武装着」和「表里还真有人收答」会漂开。漂开的后果不是多弹一张卡,是
|
|
78
|
+
* **一张都不弹**:去重短路认为卡还在,而实际上没有任何 responder 能收答([paired-mechanisms-must-
|
|
79
|
+
* share-premise])。给出这条探询口,让去重臂能问真相而不是问自己的记忆。
|
|
80
|
+
*
|
|
81
|
+
* 🔴 只回答布尔:responder 函数本身绝不出境(它闭包着 caller 的一次性状态,交出去就有第二个调用者)。
|
|
82
|
+
*/
|
|
83
|
+
export declare function hasLocalQuestionResponder(id: string): boolean;
|
|
71
84
|
/**
|
|
72
85
|
* Publish a demuxed live question frame (called from liveClient's demuxQuestionFrames on each
|
|
73
86
|
* `question`/`question_complete`). Best-effort: a UI handler error is swallowed so a broken overlay can NEVER
|
|
@@ -20,6 +20,21 @@ export function registerLocalQuestionResponder(id, fn) {
|
|
|
20
20
|
localResponders.delete(id);
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Is a local responder for this synthetic questionId still bound?([2393] hitl-F3,2026-08-02)
|
|
25
|
+
*
|
|
26
|
+
* 🔴 为什么这是**只读探询**而不是「谁注册谁自己记着就行」:上面那条契约把「对话框被 dismiss 却没
|
|
27
|
+
* 作答」的清理责任交给了 caller,而 caller(planReviewWire)自己那份武装态与这张表是**两个**容器 ——
|
|
28
|
+
* 两个容器各自都可能被单方面改动(`respondToQuestion` 的一次性 delete、别的注册把同 id 顶掉后再
|
|
29
|
+
* 注销),于是「我以为还武装着」和「表里还真有人收答」会漂开。漂开的后果不是多弹一张卡,是
|
|
30
|
+
* **一张都不弹**:去重短路认为卡还在,而实际上没有任何 responder 能收答([paired-mechanisms-must-
|
|
31
|
+
* share-premise])。给出这条探询口,让去重臂能问真相而不是问自己的记忆。
|
|
32
|
+
*
|
|
33
|
+
* 🔴 只回答布尔:responder 函数本身绝不出境(它闭包着 caller 的一次性状态,交出去就有第二个调用者)。
|
|
34
|
+
*/
|
|
35
|
+
export function hasLocalQuestionResponder(id) {
|
|
36
|
+
return localResponders.has(id);
|
|
37
|
+
}
|
|
23
38
|
/**
|
|
24
39
|
* Publish a demuxed live question frame (called from liveClient's demuxQuestionFrames on each
|
|
25
40
|
* `question`/`question_complete`). Best-effort: a UI handler error is swallowed so a broken overlay can NEVER
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* catalogLoader.ts — **目录候选链的宿主 loader**(design/166 §1-§3,2026-08-04)。
|
|
3
|
+
*
|
|
4
|
+
* ## 它与 `catalog.ts` 的分工(🔴 委托,不平行重做)
|
|
5
|
+
*
|
|
6
|
+
* `catalog.ts` 已经是三层源解析器:`resolveModelCatalog`(线上→包内→用户覆盖三层合并)+
|
|
7
|
+
* `validateOnlineCatalog`(schemaVersion 区间 / 逐行 `isProviderRow` / **绝不半解析** / 不回显载荷)
|
|
8
|
+
* + `CatalogFetchJson` 注入端口 + `CatalogSource`·`CatalogRejectReason` 两张词表。
|
|
9
|
+
* 本文件**只补 catalog.ts 声明不管的宿主面**,四件事:
|
|
10
|
+
*
|
|
11
|
+
* ① **候选链遍历**(`DEFAULT_CATALOG_SOURCES` = raw.githubusercontent → jsDelivr,可配);
|
|
12
|
+
* ② **传输安全硬门**:https + 域白名单(拨号**之前**判,域外源连请求都不发)——
|
|
13
|
+
* 而且**重定向目标过同一道门**(fetch 默认跟随重定向,不设门 = 白名单可被 302 绕过);
|
|
14
|
+
* ③ **`catalog.sha256` 旁签**(同源同路径,SHA-256 hex 比对);
|
|
15
|
+
* ④ **缓存**(`configHome/cache/model-catalog.json`)的信封与读回语义。
|
|
16
|
+
*
|
|
17
|
+
* 载荷面的判决(schemaVersion / 逐行形状 / 整份弃 / 不回显)**一行都不在这里重写** ——
|
|
18
|
+
* 本文件拿到字节流之后做的唯一一件事是 `JSON.parse`,然后把结果交给 `resolveModelCatalog`,
|
|
19
|
+
* 由它去 `validateOnlineCatalog`。UI 渲染也只认 `ModelCatalogResult.source` + `online.reason`
|
|
20
|
+
* (+ 本文件加的 `cacheHit`),**不发明第二套 origin 词表**。
|
|
21
|
+
*
|
|
22
|
+
* ## 🔴 为什么缓存的**落盘动作**在宿主而不在这里(与设计档 §3 的偏差,记在这)
|
|
23
|
+
*
|
|
24
|
+
* 设计档 §3 写的是「loader 原子写(tmp+rename)」。但本包有一条机械门守着的硬法:
|
|
25
|
+
* `src/**` 的 index 值级闭包**零 Node 内建**(`scripts/run-client-core-portability-test.mjs`
|
|
26
|
+
* ①c 段),因为同一份代码要在浏览器/桌面渲染进程里跑。`node:fs` 一进来,那道门当场红。
|
|
27
|
+
* ⇒ 落盘走**注入口** `CatalogCachePort`(与 `host.ts` 的 `FsPort`、`catalog.ts` 的
|
|
28
|
+
* `CatalogFetchJson` 同一条纪律:能力由宿主注入,库不自己 `require('fs')`)。
|
|
29
|
+
* **tmp+rename 的原子性是 `writeAtomic` 实现方的契约**,本文件在类型注释里把它写成要求;
|
|
30
|
+
* Node 宿主的十行参考实现见 `CatalogCachePort.writeAtomic` 的注释。
|
|
31
|
+
* 缺席该口 ⇒ 缓存腿整条不启用(`cacheHit` 键**缺席**,不是 `false` —— 「没查过」和「查过没用上」
|
|
32
|
+
* 是两件事,[honest-absence-not-fabricated-zero])。
|
|
33
|
+
*
|
|
34
|
+
* ## 🔴 `forceRefresh` 为什么没有(与设计档 §1 签名的偏差)
|
|
35
|
+
*
|
|
36
|
+
* 本 loader **恒先走网络**,缓存只在候选链**全败**时顶上(设计档 §3 自己就是这么定的读时机)。
|
|
37
|
+
* 也就是说没有任何一条「优先吃缓存」的路径可供 `forceRefresh` 去绕过 —— 收下这个参数只会
|
|
38
|
+
* 得到一个恒为空操作的旋钮,而空操作的旋钮是**假 affordance**(调用方以为自己强制刷新了)。
|
|
39
|
+
* 真要「手动刷新」,直接再调一次本函数就是最新语义。
|
|
40
|
+
*
|
|
41
|
+
* ## 时钟
|
|
42
|
+
*
|
|
43
|
+
* 与 `catalog.ts` 同一口径:**绝不偷读时钟**。`nowMs` 缺席 ⇒ 缓存信封不写 `fetchedAt`、
|
|
44
|
+
* 陈旧判定不做(`cacheStale` 键缺席),而不是 `Date.now()` 兜底。
|
|
45
|
+
*/
|
|
46
|
+
import { type EnvLike } from '../hostEnv.js';
|
|
47
|
+
import { type ModelCatalogResult, type OnlineCatalogDoc } from './catalog.js';
|
|
48
|
+
import type { ProviderPreset } from './providerPresets.js';
|
|
49
|
+
/**
|
|
50
|
+
* 默认候选链(design/166 §1;clay 裁「可配置,默认 github」)。
|
|
51
|
+
* 第二跳 jsDelivr 对 gh 仓是被动 CDN、零接入成本 —— 这就是大陆可达性的零维护替身。
|
|
52
|
+
* 🔴 顺序即优先级:逐源尝试,前一跳成了就不拨下一跳。
|
|
53
|
+
*/
|
|
54
|
+
export declare const DEFAULT_CATALOG_SOURCES: readonly string[];
|
|
55
|
+
/** 默认域白名单 = 默认链两跳的 host。用户显式配置的源 host 在运行期并入(用户自担)。 */
|
|
56
|
+
export declare const CATALOG_DEFAULT_HOSTS: readonly string[];
|
|
57
|
+
/** 候选链的 env 键(一键安装脚本改这里;settings.json `env` 块同名同义,见 design/166 §2)。 */
|
|
58
|
+
export declare const CATALOG_SOURCES_ENV = "SEMA_CATALOG_SOURCES";
|
|
59
|
+
/** 每源传输预算(design/166 §1:onboard 不能被网络拖住)。 */
|
|
60
|
+
export declare const DEFAULT_CATALOG_TIMEOUT_MS = 3500;
|
|
61
|
+
/** 缓存相对 configHome 的落点(design/166 §3)。 */
|
|
62
|
+
export declare const CATALOG_CACHE_RELATIVE_PATH = "cache/model-catalog.json";
|
|
63
|
+
/** 缓存陈旧阈值:30 天(照用,但如实标 stale)。 */
|
|
64
|
+
export declare const CATALOG_CACHE_STALE_MS: number;
|
|
65
|
+
/**
|
|
66
|
+
* 单源的**传输层**结局。
|
|
67
|
+
*
|
|
68
|
+
* 🔴 它**不是**第二套 origin 词表:UI 渲染「目录从哪来」恒读 `ModelCatalogResult.source` +
|
|
69
|
+
* `online.reason` + `cacheHit`。本词表只服务于 **doctor 的逐源分诊**(「两跳分别为什么没成」),
|
|
70
|
+
* 那是 `catalog.ts` 的单 URL 视角在结构上说不出来的量 —— 它只有一个 `online` 结局位。
|
|
71
|
+
*/
|
|
72
|
+
export type CatalogSourceOutcome = 'ok' | 'insecure-url' | 'host-not-allowed' | 'redirect-blocked' | 'redirect-opaque' | 'redirect-loop' | 'http-error' | 'network-error' | 'invalid-json' | 'sha-mismatch';
|
|
73
|
+
/** 一次逐源尝试的留痕(doctor 用;🔴 绝不带载荷内容,只带判决与被截断的 message)。 */
|
|
74
|
+
export interface CatalogSourceAttempt {
|
|
75
|
+
url: string;
|
|
76
|
+
outcome: CatalogSourceOutcome;
|
|
77
|
+
/** HTTP 状态码(只在真收到响应时在场)。 */
|
|
78
|
+
status?: number;
|
|
79
|
+
/** 旁签是否**真的比对过**(只在 `outcome:'ok'` 时在场;false = sha 拿不到,已记 warn 放行)。 */
|
|
80
|
+
shaChecked?: boolean;
|
|
81
|
+
/** 人话细节(判决相关短语 / 被截断的错误 message)。 */
|
|
82
|
+
detail?: string;
|
|
83
|
+
}
|
|
84
|
+
/** 缓存文件的信封(design/166 §3)。`fetchedAt` 缺席 = 写入时宿主没给时钟。 */
|
|
85
|
+
export interface CatalogCacheEnvelope {
|
|
86
|
+
fetchedAt?: number;
|
|
87
|
+
sourceUrl: string;
|
|
88
|
+
catalog: OnlineCatalogDoc;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* 缓存口 —— 宿主注入(本包零 `node:fs`,理由见文件头)。
|
|
92
|
+
*
|
|
93
|
+
* `writeAtomic` 的契约是**原子替换**(临时文件 + rename),不是 `writeFile` 的别名:
|
|
94
|
+
* 半截文件会让下一次冷启动读到一份坏缓存。Node 宿主的参考实现:
|
|
95
|
+
* ```js
|
|
96
|
+
* async writeAtomic(path, text) {
|
|
97
|
+
* await mkdir(dirname(path), { recursive: true })
|
|
98
|
+
* const tmp = `${path}.${process.pid}.tmp`
|
|
99
|
+
* await writeFile(tmp, text, 'utf8')
|
|
100
|
+
* await rename(tmp, path) // 同目录 rename = 原子
|
|
101
|
+
* }
|
|
102
|
+
* ```
|
|
103
|
+
* 两个动词都允许同步或异步实现;抛异常由 loader 收成 warning(缓存是纵深,不是主路径)。
|
|
104
|
+
*/
|
|
105
|
+
export interface CatalogCachePort {
|
|
106
|
+
read(path: string): string | null | Promise<string | null>;
|
|
107
|
+
writeAtomic(path: string, text: string): void | Promise<void>;
|
|
108
|
+
}
|
|
109
|
+
/** `loadCatalogWithSources` 的入参。 */
|
|
110
|
+
export interface LoadCatalogOptions {
|
|
111
|
+
/** 缓存落点的根(`configHome/cache/model-catalog.json`)。缺席 ⇒ 缓存腿不启用。 */
|
|
112
|
+
configHome?: string;
|
|
113
|
+
/** 覆盖候选链(缺省 `DEFAULT_CATALOG_SOURCES`;env 恒赢本项,见 `resolveCatalogSources`)。 */
|
|
114
|
+
sources?: readonly string[];
|
|
115
|
+
/** 宿主 env(缺省 `hostEnv()`)。 */
|
|
116
|
+
env?: EnvLike;
|
|
117
|
+
/** 每源传输预算(缺省 `DEFAULT_CATALOG_TIMEOUT_MS`)。 */
|
|
118
|
+
timeoutMs?: number;
|
|
119
|
+
/** 宿主时钟。缺席 ⇒ 不算 ageMs、不写 fetchedAt、不判 stale(绝不偷读时钟)。 */
|
|
120
|
+
nowMs?: number;
|
|
121
|
+
/** 传输注入口(缺省全局 `fetch`;本包既有姿势 = limitsWire/detachWire 的 `fetchImpl`)。 */
|
|
122
|
+
fetchImpl?: typeof fetch;
|
|
123
|
+
/** 缓存口。缺席 ⇒ 缓存腿整条不启用(`cacheHit` 键缺席)。 */
|
|
124
|
+
cache?: CatalogCachePort;
|
|
125
|
+
/** 用户本地覆盖层,原样透传给 `resolveModelCatalog`。 */
|
|
126
|
+
overrides?: readonly ProviderPreset[];
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* 结果 = `ModelCatalogResult`(来源标注/新鲜度/合并表全归 catalog.ts)+ 宿主面四位。
|
|
130
|
+
* 🔴 只**加**位不改位:端渲染仍读 `source` / `online.reason`。
|
|
131
|
+
*/
|
|
132
|
+
export interface LoadedModelCatalog extends ModelCatalogResult {
|
|
133
|
+
/** 真正被吃下的那一跳(线上成功或缓存自述的来源);全败 ⇒ 缺席。 */
|
|
134
|
+
sourceUrl?: string;
|
|
135
|
+
/** 这份线上载荷是不是从缓存顶上来的。**缓存口缺席 ⇒ 本键缺席**(没查过 ≠ 查过没用上)。 */
|
|
136
|
+
cacheHit?: boolean;
|
|
137
|
+
/** 缓存是否已过 30 天(照用但如实标)。只在 `cacheHit:true` 且宿主给了时钟时在场。 */
|
|
138
|
+
cacheStale?: boolean;
|
|
139
|
+
/** 逐源留痕(doctor 分诊用)。 */
|
|
140
|
+
attempts: CatalogSourceAttempt[];
|
|
141
|
+
/** 非致命的诚实记账(sha 拿不到 / 缓存读写失败 / 缓存半截)。 */
|
|
142
|
+
warnings: string[];
|
|
143
|
+
}
|
|
144
|
+
/** `SEMA_CATALOG_SOURCES` 的逗号列表解析(去空白、丢空项;缺席 ⇒ 空列表)。 */
|
|
145
|
+
export declare function parseCatalogSources(raw: string | undefined | null): string[];
|
|
146
|
+
/** `resolveCatalogSources` 的入参。 */
|
|
147
|
+
export interface ResolveCatalogSourcesOptions {
|
|
148
|
+
env?: EnvLike;
|
|
149
|
+
sources?: readonly string[];
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* 候选链优先级(design/166 §2,高→低):env `SEMA_CATALOG_SOURCES` > 调用方(settings)> 默认链。
|
|
153
|
+
* 🔴 「配了但解析出空列表」按**没配**处理:空链会把目录腿整条静默关掉,而调用方以为自己配了。
|
|
154
|
+
*/
|
|
155
|
+
export declare function resolveCatalogSources(opts?: ResolveCatalogSourcesOptions): string[];
|
|
156
|
+
/**
|
|
157
|
+
* 传输面硬门:**https + host 在白名单内**。拨号之前判 —— 不合格的地址连请求都不发。
|
|
158
|
+
* 🔴 白名单是**域**的门、https 是**协议**的门,两道都要过:白名单里的 host 走 http 照样拒
|
|
159
|
+
* (明文信道下目录可被改写成钓鱼网关,而目录决定的正是出站地址)。
|
|
160
|
+
*/
|
|
161
|
+
export declare function isAllowedCatalogUrl(url: string, allowedHosts: ReadonlySet<string>): boolean;
|
|
162
|
+
/** 旁签地址 = 同源同路径换后缀(design/165 §4:`dist/catalog.sha256`)。 */
|
|
163
|
+
export declare function catalogShaUrlFor(catalogUrl: string): string;
|
|
164
|
+
/** 缓存文件绝对路径(design/166 §3)。POSIX 分隔符 —— Node 侧 `path.join` 对它是幂等的。 */
|
|
165
|
+
export declare function catalogCachePath(configHome: string): string;
|
|
166
|
+
/**
|
|
167
|
+
* 解析出一份可用的 provider 目录 —— **候选链 + 传输硬门 + 旁签 + 缓存**,载荷判决全委托
|
|
168
|
+
* `resolveModelCatalog`。🔴 **永不 reject**:onboard 不能因为网络死。
|
|
169
|
+
*/
|
|
170
|
+
export declare function loadCatalogWithSources(opts?: LoadCatalogOptions): Promise<LoadedModelCatalog>;
|