@sema-agent/client-core 0.6.0 → 0.8.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 +31 -11
- package/dist/adapt.d.ts +4 -2
- package/dist/adapt.js +225 -25
- package/dist/compensations.d.ts +63 -0
- package/dist/compensations.js +138 -0
- package/dist/detachWire.d.ts +141 -0
- package/dist/detachWire.js +182 -0
- package/dist/diff/patch.d.ts +29 -0
- package/dist/diff/patch.js +45 -0
- package/dist/engineSessionParam.d.ts +11 -0
- package/dist/engineSessionParam.js +41 -0
- package/dist/engineWireTarget.d.ts +14 -0
- package/dist/engineWireTarget.js +53 -0
- package/dist/finalVerifyWire.d.ts +74 -0
- package/dist/finalVerifyWire.js +63 -0
- package/dist/fleet/fleetLedger.d.ts +132 -0
- package/dist/fleet/fleetLedger.js +393 -0
- package/dist/fleet/fleetProjection.d.ts +181 -0
- package/dist/fleet/fleetProjection.js +258 -0
- package/dist/headlessPermissionModeWire.d.ts +55 -0
- package/dist/headlessPermissionModeWire.js +111 -0
- package/dist/headlessReconnectWire.d.ts +96 -0
- package/dist/headlessReconnectWire.js +141 -0
- package/dist/hooksWireCaps.d.ts +91 -0
- package/dist/hooksWireCaps.js +359 -0
- package/dist/host.d.ts +34 -2
- package/dist/host.js +6 -0
- package/dist/index.d.ts +74 -1
- package/dist/index.js +104 -1
- package/dist/interactiveToolsWire.d.ts +48 -0
- package/dist/interactiveToolsWire.js +86 -0
- package/dist/limitsWire.d.ts +89 -0
- package/dist/limitsWire.js +225 -0
- package/dist/liveInitToolFace.d.ts +53 -0
- package/dist/liveInitToolFace.js +251 -0
- package/dist/model/modelFamilies.json +63 -0
- package/dist/model/providerPresets.d.ts +46 -0
- package/dist/model/providerPresets.js +160 -0
- package/dist/model/providerPresets.json +1179 -0
- package/dist/notifications.d.ts +7 -0
- package/dist/notifications.js +32 -0
- package/dist/sandboxWire.d.ts +75 -0
- package/dist/sandboxWire.js +138 -0
- package/dist/scenarioWire.d.ts +62 -0
- package/dist/scenarioWire.js +115 -0
- package/dist/seam.d.ts +52 -5
- package/dist/seam.js +15 -2
- package/dist/subagent/engineCompactWire.d.ts +12 -0
- package/dist/subagent/engineCompactWire.js +176 -0
- package/dist/subagent/engineDelegatedPrompt.d.ts +44 -0
- package/dist/subagent/engineDelegatedPrompt.js +205 -0
- package/dist/subagent/engineRowStopGate.d.ts +18 -0
- package/dist/subagent/engineRowStopGate.js +54 -0
- package/dist/subagent/engineSubagentOutput.d.ts +15 -0
- package/dist/subagent/engineSubagentOutput.js +98 -0
- package/dist/subagent/engineSubagentSteer.d.ts +10 -0
- package/dist/subagent/engineSubagentSteer.js +68 -0
- package/dist/subagent/engineSubagentTail.d.ts +40 -0
- package/dist/subagent/engineSubagentTail.js +241 -0
- package/dist/subagent/engineTaskHandleWire.d.ts +90 -0
- package/dist/subagent/engineTaskHandleWire.js +212 -0
- package/dist/toolResult.d.ts +118 -0
- package/dist/toolResult.js +774 -0
- package/dist/workflowClient.d.ts +42 -0
- package/dist/workflowClient.js +467 -0
- package/dist/workflowMonitor.d.ts +94 -0
- package/dist/workflowMonitor.js +43 -0
- package/package.json +5 -3
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { isSseIdleError, probeEngineAlive } from './sseIdleTriage.js';
|
|
2
|
+
import { hostEnv } from './hostEnv.js';
|
|
3
|
+
/** 每 run 壳级重连预算(design 铁律 5)。cap/416 的 SDK 内部续传不计入(同一次 openEvents)。 */
|
|
4
|
+
export const HEADLESS_RECONNECT_MAX = 2;
|
|
5
|
+
/** SEMA_DEBUG 探针(headless `-p` 车道 Ink 未挂载,stderr 可用;绝不带凭证/帧内容)。 */
|
|
6
|
+
function debugLog(msg) {
|
|
7
|
+
if (hostEnv().SEMA_DEBUG) {
|
|
8
|
+
// eslint-disable-next-line no-console
|
|
9
|
+
console.error(`[sema] headless reconnect: ${msg}`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** 引擎终态词表全集(core 1.298 TaskStatus;server runs.js setTerminal(taskId, safe.status, …)
|
|
13
|
+
* 把引擎 status【原样】写进 run row ⇒ row.status 可以是全部 6 词,不只 completed/failed)。
|
|
14
|
+
* [909]B3 复核发现的真缺口:此前只认 completed/failed,timeout/blocked 终局的 run row 永不
|
|
15
|
+
* 合成终帧 → 续收环重试到预算耗尽 → 假判死(终局明明就在 row 里)。 */
|
|
16
|
+
const RUN_ROW_TERMINAL_STATUSES = new Set([
|
|
17
|
+
'completed',
|
|
18
|
+
'failed',
|
|
19
|
+
'timeout',
|
|
20
|
+
'blocked',
|
|
21
|
+
'suspended',
|
|
22
|
+
'needs_review',
|
|
23
|
+
]);
|
|
24
|
+
/** run row → 合成终帧(R1b)。带 result 对象的引擎终态(全词表)合成 done{result}——与
|
|
25
|
+
* tasks/stream 终帧字节同构,adapter terminalToSdkResult 全枚举分流([909]B1)原样生效
|
|
26
|
+
* (timeout→error_max_turns、blocked→error_during_execution、park→[884]A1 臂);裸 failed
|
|
27
|
+
* (无 result)合成 failed 事件。running / 无 result 的 park(可能还会被 approvals 解锁)null
|
|
28
|
+
* —— 计预算继续重试,耗尽才判死。 */
|
|
29
|
+
export function terminalEventFromRunRow(row) {
|
|
30
|
+
if (!row || typeof row.status !== 'string')
|
|
31
|
+
return null;
|
|
32
|
+
if (RUN_ROW_TERMINAL_STATUSES.has(row.status)) {
|
|
33
|
+
if (row.result && typeof row.result === 'object') {
|
|
34
|
+
// sync 腿 setTerminal 存的就是终帧的 TaskResult(status 含 failed/timeout/blocked 形)——
|
|
35
|
+
// 与 tasks/stream 终帧 done{result} 字节同构,terminalToSdkResult 文案线原样生效。
|
|
36
|
+
return { type: 'done', result: row.result };
|
|
37
|
+
}
|
|
38
|
+
if (row.status === 'failed') {
|
|
39
|
+
return {
|
|
40
|
+
type: 'failed',
|
|
41
|
+
...(row.errorCode ? { errorCode: row.errorCode } : {}),
|
|
42
|
+
errorMessage: row.error ?? 'run failed',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* withHeadlessR1Reconnect(live, deps) — the M2 R1 lane-switch wrapper.
|
|
50
|
+
*
|
|
51
|
+
* 位置:liveClient.tasks.stream 管线【最内层】(demux/HITL 桥/captureSessionId/normalizeToolNames/
|
|
52
|
+
* diagnoseSseIdleTear 之内)——续收帧与合成终帧流经与 live 帧完全相同的下游包装(工具名规整、
|
|
53
|
+
* sessionId 捕获、HITL 桥、批 B 守卫),零特殊路径。重连成功 → 生成器以终帧正常 return ⇒
|
|
54
|
+
* diagnoseSseIdleTear/tearVerdict 永不介入,turn 正常收尾;耗尽/不可换道 → 重抛原始 SseIdleError,
|
|
55
|
+
* 外层打标 + 现行判死文案,字节不变。
|
|
56
|
+
*/
|
|
57
|
+
export async function* withHeadlessR1Reconnect(live, deps) {
|
|
58
|
+
let originalTearErr;
|
|
59
|
+
try {
|
|
60
|
+
for await (const ev of live) {
|
|
61
|
+
yield ev;
|
|
62
|
+
const t = ev?.type;
|
|
63
|
+
if (t === 'done' || t === 'failed')
|
|
64
|
+
return; // 终帧已到 —— 首终帧即收官(批 B 双帧守卫同侧)
|
|
65
|
+
}
|
|
66
|
+
return; // 流干净收尾(mock 车道/abort return)——无终帧也不由本腿补,现行行为不变
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
originalTearErr = e;
|
|
70
|
+
}
|
|
71
|
+
// ── 重试环:每轮入口按铁律顺序分诊,非命中一律重抛【原始】错误(现行失败路径字节不变)──
|
|
72
|
+
let attempts = 0;
|
|
73
|
+
let lastSeenSeq; // R1 去重锚(runs.events SSE id = 账本 seq;见模块头选型)
|
|
74
|
+
const probe = deps.probeAlive ?? probeEngineAlive;
|
|
75
|
+
for (;;) {
|
|
76
|
+
if (deps.signal?.aborted)
|
|
77
|
+
throw originalTearErr; // 铁律 3:用户中止恒先行,绝不进重连分支
|
|
78
|
+
if (!isSseIdleError(originalTearErr))
|
|
79
|
+
throw originalTearErr; // 只救 engine-alive-idle 的静默撕裂形
|
|
80
|
+
const taskId = deps.getTaskId();
|
|
81
|
+
if (!taskId)
|
|
82
|
+
throw originalTearErr; // X-Task-Id 未捕获(首轮早期窗)——无从换道,接受
|
|
83
|
+
if (attempts >= HEADLESS_RECONNECT_MAX) {
|
|
84
|
+
debugLog(`budget exhausted (${attempts}/${HEADLESS_RECONNECT_MAX}) — falling to the honest death path`);
|
|
85
|
+
throw originalTearErr; // 耗尽 → 现行 engine-alive-idle 判死(文案已诚实,M1)
|
|
86
|
+
}
|
|
87
|
+
attempts++;
|
|
88
|
+
if (!(await probe(deps.baseUrl)))
|
|
89
|
+
throw originalTearErr; // 铁律 5:引擎死 → 现行 engine-down 路径
|
|
90
|
+
debugLog(`attempt ${attempts}/${HEADLESS_RECONNECT_MAX} — switching to runs.events(${taskId}${lastSeenSeq !== undefined ? `, Last-Event-ID ${lastSeenSeq}` : ''})`);
|
|
91
|
+
try {
|
|
92
|
+
const cont = deps.openEvents(taskId, lastSeenSeq !== undefined ? String(lastSeenSeq) : undefined);
|
|
93
|
+
for await (const ev of cont) {
|
|
94
|
+
if (deps.signal?.aborted)
|
|
95
|
+
return; // 中止 → 干净收流(print.ts SIGINT 处置 + M1 cancel 兜底接管)
|
|
96
|
+
const rec = ev;
|
|
97
|
+
const t = rec?.type;
|
|
98
|
+
if (t === undefined)
|
|
99
|
+
continue; // 心跳帧(data:{})——车道保活杂音,不进消费管线
|
|
100
|
+
if (t === 'error') {
|
|
101
|
+
// STREAM_MAX_DURATION cap 帧(server streamSseLog):SDK parseSse 已锁存 Last-Event-ID
|
|
102
|
+
// 并自动重开(同一次 openEvents 内部,计入同一次预算)——本帧是车道协议杂音,不下发。
|
|
103
|
+
debugLog(`runs.events lane frame type=error code=${String(rec?.code ?? '')} — SDK resumes with Last-Event-ID internally`);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const seq = typeof rec?.id === 'string' && /^\d+$/.test(rec.id) ? Number(rec.id) : undefined;
|
|
107
|
+
if (seq !== undefined) {
|
|
108
|
+
if (lastSeenSeq !== undefined && seq <= lastSeenSeq)
|
|
109
|
+
continue; // 重放帧 → seq 锚去重(防双投)
|
|
110
|
+
lastSeenSeq = seq;
|
|
111
|
+
}
|
|
112
|
+
// 终帧日志必须在 yield 之前:下游(adapter runStream)拿到首终帧即 return 弃养本生成器,
|
|
113
|
+
// yield 之后的语句不再执行(受控腿 leg 1 实测)。return 语义不受影响(弃养即收官)。
|
|
114
|
+
if (t === 'done' || t === 'failed')
|
|
115
|
+
debugLog(`resumed to terminal (${String(t)}) after ${attempts} reconnect(s)`); // 判死解除
|
|
116
|
+
yield ev;
|
|
117
|
+
if (t === 'done' || t === 'failed')
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
// 无终帧收流(SDK 只在 signal.aborted 时干净 return;防御分支)——按撕裂再走一轮预算
|
|
121
|
+
if (deps.signal?.aborted)
|
|
122
|
+
return;
|
|
123
|
+
debugLog('runs.events continuation ended without a terminal frame');
|
|
124
|
+
}
|
|
125
|
+
catch (contErr) {
|
|
126
|
+
// 续收环失败。现役 server sync 腿账本为空 ⇒ run 落终局后 SDK 以「无进展 5 连败」收场
|
|
127
|
+
// ——这不是死刑:终局已在 run row(R1b),查得到就合成终帧收官。
|
|
128
|
+
debugLog(`runs.events continuation failed: ${contErr instanceof Error ? contErr.message : String(contErr)}`);
|
|
129
|
+
if (deps.signal?.aborted)
|
|
130
|
+
throw originalTearErr; // 铁律 3:中止后不捞终局,交给上游中止处置
|
|
131
|
+
const row = await deps.getRun(taskId).catch(() => null);
|
|
132
|
+
const terminal = terminalEventFromRunRow(row);
|
|
133
|
+
if (terminal) {
|
|
134
|
+
debugLog(`run row terminal (${String(row?.status)}) — synthesizing the terminal frame (R1b)`);
|
|
135
|
+
yield terminal;
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// run 还在跑 / suspended / row 读不到 —— 计预算继续重试(环顶做 aborted/预算/探活分诊)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { type EnvLike } from './hostEnv.js';
|
|
2
|
+
import type { WireHooksConfig } from './finalVerifyWire.js';
|
|
3
|
+
export type { WireHooksConfig };
|
|
4
|
+
/** The env ESCAPE HATCH for the engine leg (see the block comment above). */
|
|
5
|
+
export declare const GOAL_STOP_HOOK_WIRE_ENV = "SEMA_GOAL_STOP_HOOK_WIRE";
|
|
6
|
+
export declare const CC_STOP_SEMANTICS_MIN_SERVER = "1.279.3";
|
|
7
|
+
/**
|
|
8
|
+
* 引擎自报版本是否达到 CC Stop 语义线({@link CC_STOP_SEMANTICS_MIN_SERVER})。
|
|
9
|
+
*
|
|
10
|
+
* 🔴 三条纪律:
|
|
11
|
+
* · **数值比较**,不是字典序(`1.9.0` < `1.279.0`,字符串比较会判反);
|
|
12
|
+
* · **预发版不算达标**(`1.279.0-rc.1` 里那套可能还在改)——正则只认三段纯数字,带 `-` 的直接落
|
|
13
|
+
* null ⇒ false;
|
|
14
|
+
* · 读不出/探不到 ⇒ **false**(降级方向恒安全:老形在新引擎上仍能拦,新形在老引擎上一次都拦不住)。
|
|
15
|
+
*/
|
|
16
|
+
export declare function ccStopSemanticsFromVersion(version: string | undefined | null): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* 本会话连着的引擎是否具备 CC Stop 语义?判据 = live 车道的 `/v1/capabilities` 自报 `version`
|
|
19
|
+
* (engineCapsCache 的 boot-kick 缓存,与 `/health.version` 同源)。
|
|
20
|
+
*
|
|
21
|
+
* 🔴 「未判即降级」:caps 探测是构造期 fire-and-forget,`/goal` 真被敲下时通常早已落袋;万一没落袋
|
|
22
|
+
* 就按老引擎走(meta 也同步退档)——**宁可少承诺,不可多承诺**。
|
|
23
|
+
*/
|
|
24
|
+
export declare function engineCcStopSemantics(env?: EnvLike): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Is the `/goal` Stop-hook engine leg armed? **Default ON** since 2026-07-26 — both of the premises
|
|
27
|
+
* that kept it opt-in were disproved by server-side behavior evidence ([1721]) and re-verified against
|
|
28
|
+
* a real engine + real model (cli scripts/run-goal-stop-hook-live-test.mjs). Set
|
|
29
|
+
* `SEMA_GOAL_STOP_HOOK_WIRE=0|false|no|off` to disarm (an operator escape hatch, not a default).
|
|
30
|
+
*/
|
|
31
|
+
export declare function isGoalStopHookWireArmed(env?: EnvLike): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Register (or clear, with `null`) the session-scoped `/goal` Stop prompt hook that rides
|
|
34
|
+
* `settings.hooks.Stop` to the engine. Single-valued: a new goal replaces the previous one,
|
|
35
|
+
* exactly like CC 220's `Xdr` (remove-then-add). Projection is gated by
|
|
36
|
+
* {@link isGoalStopHookWireArmed} — storing is always safe, sending is not (yet).
|
|
37
|
+
*/
|
|
38
|
+
export declare function setWireSessionStopHook(prompt: string | null, opts?: {
|
|
39
|
+
/**
|
|
40
|
+
* 这条目标登记时,引擎是否已判定为 CC 语义档({@link engineCcStopSemantics})?
|
|
41
|
+
*
|
|
42
|
+
* 🔴 **为什么要在 set 那一刻锁存,而不是在 {@link hooksForWire} 里现读**:`/goal` 的 meta
|
|
43
|
+
* (对模型讲「会不会拦到条件成立为止」)是在 set 那一刻发出去的,而投影发生在随后每个 turn。
|
|
44
|
+
* 两处各读一次的话,caps 探测中途落袋就会造出「meta 承诺拦到底、投出去的却还是老门」的错配 ——
|
|
45
|
+
* 判据必须锚在**真正决定结果的那个量**上,而它只有一个:**这条目标登记时的档位**。
|
|
46
|
+
* 缺省 false ⇒ 不传就是老引擎档:翻档只能靠证据打开,不能靠默认值。
|
|
47
|
+
*/
|
|
48
|
+
ccSemantics?: boolean;
|
|
49
|
+
}): void;
|
|
50
|
+
/** The currently registered `/goal` Stop prompt, or null (registered ≠ projected — see the gate). */
|
|
51
|
+
export declare function getWireSessionStopHook(): string | null;
|
|
52
|
+
/** 当前这条目标登记时锁存的档位(真 = 投裸条件,假 = 投壳自造的门)。 */
|
|
53
|
+
export declare function getWireSessionStopHookCcSemantics(): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* 把 `/goal` 的**原始条件**包成一条对引擎 prompt 载体**可执行**的 Stop 门 prompt。
|
|
56
|
+
*
|
|
57
|
+
* 🔴 适用面(2026-07-26 提货批后):**只用于未达标线的引擎档**(见 {@link CC_STOP_SEMANTICS_MIN_SERVER})。
|
|
58
|
+
* 达标档起引擎自己按 CC
|
|
59
|
+
* 220 逐字组装(系统提示 + 会话转录 + `wrapCondition`),那一档投的是**裸条件** —— 见
|
|
60
|
+
* {@link ccStopSemanticsFromVersion} 与 {@link hooksForWire} 的分道。本函数保留,是 tolerate-absent
|
|
61
|
+
* 的实体:老引擎上退回它,仍能拦一次;删掉它等于在老引擎上静默失效。
|
|
62
|
+
*
|
|
63
|
+
* 🔴 为什么必须包(2026-07-26 live 围栏实测,不是设计偏好):CC 单进程的 prompt 钩子执行器
|
|
64
|
+
* (`src/utils/hooks/execPromptHook.ts`)会给条件**套一层系统提示**(「You are evaluating a hook…
|
|
65
|
+
* 回 `{ok:true}` / `{ok:false,reason}`」)**并把整段会话前置**,所以裸条件在 CC 那边能被真评估。
|
|
66
|
+
* 引擎侧的载体两样都没有:
|
|
67
|
+
* · 无系统提示、无会话历史;
|
|
68
|
+
* · Stop payload 的 `transcript_path` 是**空串**(server basePayload),评估者对本会话是**瞎的**;
|
|
69
|
+
* · 判据 schema 也不同 —— core 只认 stdout JSON 的 `decision:"block"`。
|
|
70
|
+
* 实测(钉版 server 1.277.1 + core 1.416 + 真模型):裸条件 `the project contains a file named
|
|
71
|
+
* DONE.txt` 送过去,载体照样被调用,模型回的是**散文**(「I don't have direct access to your file
|
|
72
|
+
* system…」)⇒ `parseHookStdout` 拿不到 decision ⇒ **一次都不拦**。即:不包 = 每次停机白烧一次
|
|
73
|
+
* 模型调用、零效果。
|
|
74
|
+
*
|
|
75
|
+
* 所以这里把条件包成引擎契约里**能落地**的形状。诚实标注它**够不到 CC 的完整语义**:
|
|
76
|
+
* · 能做到:每个 turn 的**第一次**停机尝试被拦下,目标原文重新注入给模型,逼它自查后再收尾
|
|
77
|
+
* (`stop_hook_active` 为真时门自己收手 ⇒ 不会无限拦停);真正「条件是否成立」的判断交给
|
|
78
|
+
* **有会话、有工具的主模型**,而不是让瞎的评估者假装判断;
|
|
79
|
+
* · 做不到:CC 那种「拦到条件真成立为止」(需要引擎把 transcript 交给载体)、以及「条件满足后
|
|
80
|
+
* 自动清除目标」。这两条是**等件**,meta 文案因此不照抄 CC `Pwo`(见 cmd-goal.tsx)。
|
|
81
|
+
*
|
|
82
|
+
* 台账仍存**原始条件**({@link getWireSessionStopHook}),所以 `/goal clear` 读回的是用户写的那句。
|
|
83
|
+
*/
|
|
84
|
+
export declare function buildGoalStopHookPrompt(condition: string): string;
|
|
85
|
+
/**
|
|
86
|
+
* The merged, GOVERNANCE-GATED settings-file hooks for the wire, or undefined when none apply.
|
|
87
|
+
* Per-event arrays concatenate across sources (policy → user → project → local).
|
|
88
|
+
*/
|
|
89
|
+
export declare function hooksForWire(): WireHooksConfig | undefined;
|
|
90
|
+
/** 测试钩:清 `/goal` 锁存(壳里靠进程边界隔离;包内同进程多组断言必须能清)。 */
|
|
91
|
+
export declare function __resetHooksWireCapsForTests(): void;
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⇄ B6 批搬迁(2026-07-27,设计稿 §3 B6 · B2 判给后批的**宿主耦合六件**之一,也是最后一件大的):
|
|
3
|
+
* cli `src/sema/hooksWireCaps.ts` 整搬。B2 当时不搬的唯一理由 = 它读三层 settings 文件 +
|
|
4
|
+
* 工作区信任门,而那时还没有 `SettingsPort`;B4 把口定出来了(host.ts `SettingsPort`),本批接线。
|
|
5
|
+
*
|
|
6
|
+
* 🔴 搬迁四处差分(端资产上移到 port,行为一字节不变):
|
|
7
|
+
* ① `getSettingsForSource`(cli `utils/settings/settings.ts`)→ `hostSettings()?.getSettingsForSource`;
|
|
8
|
+
* ② `shouldSkipHookDueToTrust`(cli `utils/hooks.ts`,原文用 **lazy `require()`** 避免把大图拖进
|
|
9
|
+
* 静态依赖)→ `hostSettings()?.shouldSkipHookDueToTrust` —— port 化之后 lazy require 的理由
|
|
10
|
+
* 连同 `require` 本身一起消失(本包零 CJS,portability 门的产物断言盯着 `require(`);
|
|
11
|
+
* ③ `logForDebugging` → `hostLog('debug', …)`;
|
|
12
|
+
* ④ `process.env` 缺省参 → `hostEnv()`。
|
|
13
|
+
*
|
|
14
|
+
* 🔴 **SettingsPort 缺席的后果不是「少读一层配置」,是把治理门整个拆了**:
|
|
15
|
+
* `shouldSkipHookDueToTrust` 是**安全门**(未受信工作区的 hook 绝不上 wire)。端不实现它 =
|
|
16
|
+
* 信任门在引擎腿上不存在。所以本文件在 port 缺席时**整体 fail-closed 返回 undefined**
|
|
17
|
+
* (= 一条 hook 都不投),而不是「读不到 settings 就当没有治理策略、把 goal 钩子照投」。
|
|
18
|
+
* ⚠️ 这与壳侧原文那条「早启动就 fall through」的 catch **不同**,是本批唯一的语义差:
|
|
19
|
+
* 壳里那条 catch 的前提是「本地执行器仍会 per-fire 把门」(单进程);库里没有那个兜底,
|
|
20
|
+
* 端要么装 port 要么这条腿不启用。`hostPortMisses().settings` 会把漏装喊出来。
|
|
21
|
+
*
|
|
22
|
+
* 🔴 模块级状态:`wireSessionStopHookPrompt` / `wireSessionStopHookCcSemantics`(`/goal` 的会话级
|
|
23
|
+
* Stop 钩子锁存)——写口 `setWireSessionStopHook`(命令层)、读口 `hooksForWire`(每 turn 请求
|
|
24
|
+
* 构造期)与 `getWireSessionStopHook`(`/goal clear` 回显)全在本文件。两份实例 = 设了目标却
|
|
25
|
+
* 投不出去(而两边代码看着都对)。单实例清单新增项。
|
|
26
|
+
*
|
|
27
|
+
* ── 以下为原文件的领域说明(逐字保留)────────────────────────────────────────────────────────
|
|
28
|
+
*
|
|
29
|
+
* hooksWireCaps — project the user's settings-file hooks onto the wire (`TaskRequest.settings.hooks`).
|
|
30
|
+
*
|
|
31
|
+
* service 1.131.0 hook-runner 阶段一([495]①,契约 = @sema-ai/registry-core/hooks 0.1.51, CC 2.1.198
|
|
32
|
+
* 逐字形状):引擎消费 `body.settings.hooks`,当前点亮 PreToolUse + PostToolUse × command 条目(真
|
|
33
|
+
* /bin/sh -c,exit-2 阻断 stderr 喂模型,matcher 精确/`a|b`/锚定正则);其余事件配置合法不点亮
|
|
34
|
+
* (`hook_events_not_fired` 记账,后续阶段自动亮)。这正是 live 模式的空缺半场:工具在引擎执行,
|
|
35
|
+
* 壳本地永远 fire 不了工具链 hooks — 30 事件枚举里的 ENGINE_OWNED 常量(rc.36 壳半场)壳侧只做
|
|
36
|
+
* schema 合法,不触发,所以整份投影零双跑。
|
|
37
|
+
*
|
|
38
|
+
* GOVERNANCE GATES (对抗复审 rc.41 review HIGH — the wire must honor the SAME gates the local executor
|
|
39
|
+
* honors, or a fleet-managed policy is silently bypassed by the engine leg):
|
|
40
|
+
* - workspace trust: interactive sessions with the trust dialog unaccepted ship NO hooks
|
|
41
|
+
* (shouldSkipHookDueToTrust — the rc.36 "hooks 全被 trust 门禁" invariant, engine leg included);
|
|
42
|
+
* - `disableAllHooks` (managed/policy settings): ships NO hooks;
|
|
43
|
+
* - `allowManagedHooksOnly`: ONLY the policy-settings hooks ship; user/project/local are blocked.
|
|
44
|
+
*
|
|
45
|
+
* Projection semantics (CC settings merge, verbatim shapes):
|
|
46
|
+
* - Sources: policy → user → project → local settings files, per-event arrays CONCATENATED in that
|
|
47
|
+
* order (CC's own multi-source hook semantics — later scopes append, they don't shadow).
|
|
48
|
+
* - Values pass through AS-IS including malformed entries (对抗复审 MED: dropping them client-side is a
|
|
49
|
+
* silent-fallback; the service 400s fail-loud with a field path, which the turn error card shows).
|
|
50
|
+
* - Presence is the opt-in: no hooks configured ⇒ undefined ⇒ no stamp ⇒ request shape unchanged.
|
|
51
|
+
* - Session-scoped runtime hooks and plugin/skill hooks are NOT projected in phase one (they carry
|
|
52
|
+
* pluginRoot/skillRoot expansion contexts the wire contract doesn't model yet).
|
|
53
|
+
* - Multi-tenant deployments advertise `taskSettings.hooks:false` and warn+ignore server-side, so
|
|
54
|
+
* the shell does not pre-gate on capabilities (single-user TOC is always true — [495]① note ①).
|
|
55
|
+
*
|
|
56
|
+
* Secrets: hook processes run under the SERVICE's env allowlist (PATH/HOME/locale + CLAUDE_PROJECT_DIR)
|
|
57
|
+
* and never inherit engine secrets; user-defined variables must ride `settings.env` — nothing for the
|
|
58
|
+
* shell to scrub here (commands are the user's own text).
|
|
59
|
+
*/
|
|
60
|
+
import { hostLog, hostSettings } from './host.js';
|
|
61
|
+
import { hostEnv } from './hostEnv.js';
|
|
62
|
+
import { engineCapString } from './engineCapsCache.js';
|
|
63
|
+
const EDITABLE_HOOK_SOURCES = ['userSettings', 'projectSettings', 'localSettings'];
|
|
64
|
+
function hooksOfSource(source) {
|
|
65
|
+
try {
|
|
66
|
+
const s = hostSettings()?.getSettingsForSource(source);
|
|
67
|
+
return s?.hooks && typeof s.hooks === 'object' && !Array.isArray(s.hooks) ? s.hooks : null;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// ── SESSION-SCOPED Stop hook overlay(`/goal`,任务 #75 · C-7,2026-07-26)───────────────────────
|
|
74
|
+
// CC 220 的 `/goal <condition>` 会往 `sessionHooksRegistry` 注册一条会话级 Stop×prompt 钩子
|
|
75
|
+
// (`t.sessionHooksRegistry.add(sessionId,"Stop","",{type:"prompt",prompt})`),那才是「干到条件满足
|
|
76
|
+
// 再停」的机制半边。CC 是单进程,注册进壳内 registry 就够。
|
|
77
|
+
//
|
|
78
|
+
// 🔴 **sema 是双进程壳,壳侧那条钩子在 live 模式 fire 不到**(实证,不是推测):壳的 Stop 钩子只有
|
|
79
|
+
// `src/query/stopHooks.ts handleStopHooks` 一个执行点,调用方是 `src/query.ts` —— 壳自己的 query
|
|
80
|
+
// 循环;live 模式 turn 循环在引擎里跑,壳的 query.ts 不参与。upstreamBridge 只 fire SubagentStop 且
|
|
81
|
+
// observe-only。所以引擎腿必须走 `settings.hooks` 投影,这个 overlay 就是那条腿。
|
|
82
|
+
//
|
|
83
|
+
// 🟢 **本 overlay 默认开**(2026-07-26 翻默认;逃生口 `SEMA_GOAL_STOP_HOOK_WIRE=0/false/no/off`)。
|
|
84
|
+
//
|
|
85
|
+
// 修前默认关,理由是两条**前提**——两条都已被 server 侧行为取证**证伪**(黑板 [1721],6 条行为
|
|
86
|
+
// 用例已进 server 仓;cli 仓另有 scripts/run-goal-stop-hook-live-test.mjs 的真引擎+真模型复验):
|
|
87
|
+
// ① 旧论断「SDK 类型 DEFERRED ⇒ 服务端会 400」**是把类型面当成了运行时校验面**,对 server
|
|
88
|
+
// 不成立:`parseHooksConfig({Stop:[{hooks:[{type:"prompt",prompt}]}]})` 无 error、真解析;
|
|
89
|
+
// prompt 形在 server 的条目白名单里与 command/http/agent **并列**
|
|
90
|
+
// (@sema-agent/server hooks/hook-runner `entry.type !== "command" && … !== "prompt" && …`
|
|
91
|
+
// 那道 type_not_supported 闸口把 prompt 放行,1.277.1 dist 对应 hook-runner.js:390)。
|
|
92
|
+
// 卡着的只是 **SDK / registry-core 的类型**(见下方 cast),与 server 运行时接受与否两件事。
|
|
93
|
+
// ② 旧论断「引擎侧 Stop×prompt 是否真 fire 未取证」已取证:**真 fire**(Stop 触发时 prompt
|
|
94
|
+
// 载体调用一次,`$ARGUMENTS` 已替换为 payload JSON)、**能拦停**(模型输出
|
|
95
|
+
// `{"decision":"block","reason":…}` → `{block: reason}`,core `runner/runtask.js:1663`
|
|
96
|
+
// `(spec.hooks ?? deps.hooks)?.stop` 起 stop gate,整条链通)、非决策 JSON 不拦(与 command
|
|
97
|
+
// 形 exit-0 非 JSON 同语义)、`stop_hook_active:true` 如实进 payload(模型可自己收手;core
|
|
98
|
+
// 另有 STOP_HOOK_BLOCK_CAP=8 连续拦停硬上限兜底,不会无限拦停)。
|
|
99
|
+
// ③ 最坏情况**不是** 400,而是**钩子不生效**:部署没组装 prompt 载体(`ctx.hookLlm` 缺席)⇒
|
|
100
|
+
// server 记账 `hook_entries_skipped{reason:"llm_carrier_unavailable"}` 并跳过,**非阻断**。
|
|
101
|
+
// ⇒ 默认开。等件(不阻断本腿):registry-core hooks 契约建模 prompt 形([1717]① 在飞)、SDK
|
|
102
|
+
// `SettingsHookCommand` 放开 prompt 形 —— 到位后把下方 cast 换成直类型即可。
|
|
103
|
+
//
|
|
104
|
+
// 纪律:
|
|
105
|
+
// - 只放 goal 这一条(单值,不是通用会话钩子通道)——通用会话/插件钩子仍按模块头所述不投影;
|
|
106
|
+
// - 走 hooksForWire() 的**同一套治理门**(trust / disableAllHooks / allowManagedHooksOnly):
|
|
107
|
+
// overlay 在门之后才合并,策略禁钩子时一并禁掉;
|
|
108
|
+
// - `/goal clear` 传 null 收回。
|
|
109
|
+
let wireSessionStopHookPrompt = null;
|
|
110
|
+
/**
|
|
111
|
+
* CC 语义档的**锁存位**(见 {@link setWireSessionStopHook})。投影形按它分道:
|
|
112
|
+
* 真 ⇒ 裸条件(引擎自己包 CC 形);假 ⇒ 壳自造的门 prompt(老引擎唯一能落地的形)。
|
|
113
|
+
*/
|
|
114
|
+
let wireSessionStopHookCcSemantics = false;
|
|
115
|
+
/** The env ESCAPE HATCH for the engine leg (see the block comment above). */
|
|
116
|
+
export const GOAL_STOP_HOOK_WIRE_ENV = 'SEMA_GOAL_STOP_HOOK_WIRE';
|
|
117
|
+
// ── 引擎档位:CC 220 Stop×prompt 完整语义(达标线 = CC_STOP_SEMANTICS_MIN_SERVER,见下)────────
|
|
118
|
+
//
|
|
119
|
+
// server 1.279.0 把 CC 220 的 prompt-hook 半场搬进了引擎(dist/hooks/cc-stop-prompt.js +
|
|
120
|
+
// branch-transcript.js,源码直读):
|
|
121
|
+
// ① Stop×prompt 触发时先取 `sctx.getBranch()`(core 1.419+ 就在 stop gate ctx 里传了它)渲染成
|
|
122
|
+
// 会话转录;**证据面为空则整条 prompt 条目被过滤掉、评估者根本不被调用**
|
|
123
|
+
// (`hook_entries_skipped{reason:"stop_prompt_no_evidence:<why>"}`)——这正是老引擎那条
|
|
124
|
+
// 「白烧一次模型调用、零效果」的反面;
|
|
125
|
+
// ② 调用时带 CC 220 **逐字**的系统提示(`ccPromptSystemFor("Stop")` = CC 的 full 档)+ CC 逐字的
|
|
126
|
+
// 条件包装(`wrapCondition`);
|
|
127
|
+
// ③ 判词 `{ok,reason,impossible}` 在**引擎翻译层**映射:ok ⇒ 放行、impossible ⇒ 记 warn 后**放行**
|
|
128
|
+
// (真 fail-open,既不 block 也不产 additionalContext)、其余 ⇒ block(reason 喂回模型)。
|
|
129
|
+
//
|
|
130
|
+
// ⇒ 到这一档,壳侧唯一正确的动作是把 CC `Xdr` 的**原形(裸条件)**投出去 —— 引擎会自己包。继续投
|
|
131
|
+
// 壳自造的门 prompt 只会被再包一层、并与 CC 系统提示要求的判词 schema 打架。
|
|
132
|
+
// 🔴 但老引擎**没有这套**(1.278.0 的 hooks/ 目录里既无 cc-stop-prompt 也无 branch-transcript,
|
|
133
|
+
// hook-llm 连 `system` 形参都没有):裸条件送过去评估者对本会话是瞎的,回散文 ⇒ 一次都不拦。
|
|
134
|
+
// 所以判档失败/探不到时**必须**退回老形,绝不「因为代码里写了 enforced 就真拦」。
|
|
135
|
+
//
|
|
136
|
+
// 🔴 **为什么达标线钉 1.279.3 而不是 CC 面首次出现的 1.279.0** —— 两道闸,缺一道都不许翻档:
|
|
137
|
+
//
|
|
138
|
+
// ① **core 下限**(1.279.1 才补上):CC Stop 语义在 1.279.0 就在了,但 1.279.1 才把
|
|
139
|
+
// `@sema-agent/core` 的下限提到 `^1.422.0`。core <1.422 上,一条只带 `additionalContext` 的
|
|
140
|
+
// Stop 反馈**不结束 + 不计数 + 清零计数** ⇒ block 与 additionalContext 交替时计数呈锯齿
|
|
141
|
+
// (`[0,1,0,1,0]`)、`STOP_HOOK_BLOCK_CAP` 永远到不了 ⇒ 无界循环;1.422 起它计入上限且不再清零。
|
|
142
|
+
// (注:goal 这条路本身产不出 additionalContext —— CC 判词的 ok/impossible 两支在引擎翻译层都
|
|
143
|
+
// 直接 `continue`;但那条通道对 command 形钩子是开着的,而**上限修好的是最坏值,不是这条通道的
|
|
144
|
+
// 性质**:它仍然会让那一轮不结束。所以措辞面一步都不许放松,见 cmd-goal 的 meta 四档。)
|
|
145
|
+
//
|
|
146
|
+
// ② **判词额度**(1.279.3 才补上):1.279.1/1.279.2 的引擎侧 hook 载体把 `max_tokens` 钳死在
|
|
147
|
+
// `Math.min(…, 1024)`(dist/hooks/hook-llm.js)。而 CC 的评估者系统提示要求「quote evidence
|
|
148
|
+
// from the transcript」,推理档模型的 thinking 还与输出**共用**这份额度 ⇒ 判词被截断(JSON 不
|
|
149
|
+
// 闭合)或干脆零 text 块 ⇒ `parseCcVerdict` 拿不到 ⇒ 引擎**丢弃整条判词并 fail-open 放行**。
|
|
150
|
+
// 🔴 这条缺口的形状与 ① 不同,但危害更隐蔽:它让 enforced 档对模型讲的那两句承诺在**守卫失效
|
|
151
|
+
// 的那些轮里变成空话,而且没有任何人知道**(引擎侧只留一行 `hook_llm_unparsed_verdict` /
|
|
152
|
+
// `hook_llm_failed{returned no content}` 记账,壳侧、用户侧、模型侧全都看不到)。
|
|
153
|
+
// 1.279.3 起 `maxOutputTokens` 由调用方声明,CC 评估者路声明 `CC_EVALUATOR_MAX_OUTPUT_TOKENS`
|
|
154
|
+
// = 4096,并对「无内容」重试一次(**不**重试「有内容但读不懂」)。
|
|
155
|
+
//
|
|
156
|
+
// ⇒ 达标线钉的是「**承诺能兑现**的那个组合」,不是「能力面在场」的那一版。差一档就翻,等于把用户
|
|
157
|
+
// 放到一个「守卫会静默失效」的搭配上,还对着模型把话说满。
|
|
158
|
+
export const CC_STOP_SEMANTICS_MIN_SERVER = '1.279.3';
|
|
159
|
+
/** `x.y.z` → 数值三元组;非法形 ⇒ null(预发标记按「未达标」处理,见下)。 */
|
|
160
|
+
function parseSemverCore(v) {
|
|
161
|
+
const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(v.trim());
|
|
162
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* 引擎自报版本是否达到 CC Stop 语义线({@link CC_STOP_SEMANTICS_MIN_SERVER})。
|
|
166
|
+
*
|
|
167
|
+
* 🔴 三条纪律:
|
|
168
|
+
* · **数值比较**,不是字典序(`1.9.0` < `1.279.0`,字符串比较会判反);
|
|
169
|
+
* · **预发版不算达标**(`1.279.0-rc.1` 里那套可能还在改)——正则只认三段纯数字,带 `-` 的直接落
|
|
170
|
+
* null ⇒ false;
|
|
171
|
+
* · 读不出/探不到 ⇒ **false**(降级方向恒安全:老形在新引擎上仍能拦,新形在老引擎上一次都拦不住)。
|
|
172
|
+
*/
|
|
173
|
+
export function ccStopSemanticsFromVersion(version) {
|
|
174
|
+
if (typeof version !== 'string' || version.length === 0)
|
|
175
|
+
return false;
|
|
176
|
+
const got = parseSemverCore(version);
|
|
177
|
+
const min = parseSemverCore(CC_STOP_SEMANTICS_MIN_SERVER);
|
|
178
|
+
if (got === null || min === null)
|
|
179
|
+
return false;
|
|
180
|
+
for (let i = 0; i < 3; i++) {
|
|
181
|
+
if (got[i] !== min[i])
|
|
182
|
+
return got[i] > min[i];
|
|
183
|
+
}
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* 本会话连着的引擎是否具备 CC Stop 语义?判据 = live 车道的 `/v1/capabilities` 自报 `version`
|
|
188
|
+
* (engineCapsCache 的 boot-kick 缓存,与 `/health.version` 同源)。
|
|
189
|
+
*
|
|
190
|
+
* 🔴 「未判即降级」:caps 探测是构造期 fire-and-forget,`/goal` 真被敲下时通常早已落袋;万一没落袋
|
|
191
|
+
* 就按老引擎走(meta 也同步退档)——**宁可少承诺,不可多承诺**。
|
|
192
|
+
*/
|
|
193
|
+
export function engineCcStopSemantics(env = hostEnv()) {
|
|
194
|
+
const base = env.SEMA_LIVE_BASEURL;
|
|
195
|
+
if (!base)
|
|
196
|
+
return false;
|
|
197
|
+
return ccStopSemanticsFromVersion(engineCapString(base, 'version'));
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Is the `/goal` Stop-hook engine leg armed? **Default ON** since 2026-07-26 — both of the premises
|
|
201
|
+
* that kept it opt-in were disproved by server-side behavior evidence ([1721]) and re-verified against
|
|
202
|
+
* a real engine + real model (cli scripts/run-goal-stop-hook-live-test.mjs). Set
|
|
203
|
+
* `SEMA_GOAL_STOP_HOOK_WIRE=0|false|no|off` to disarm (an operator escape hatch, not a default).
|
|
204
|
+
*/
|
|
205
|
+
export function isGoalStopHookWireArmed(env = hostEnv()) {
|
|
206
|
+
const v = env[GOAL_STOP_HOOK_WIRE_ENV]?.trim().toLowerCase();
|
|
207
|
+
if (v === undefined || v === '')
|
|
208
|
+
return true;
|
|
209
|
+
return !(v === '0' || v === 'false' || v === 'no' || v === 'off');
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Register (or clear, with `null`) the session-scoped `/goal` Stop prompt hook that rides
|
|
213
|
+
* `settings.hooks.Stop` to the engine. Single-valued: a new goal replaces the previous one,
|
|
214
|
+
* exactly like CC 220's `Xdr` (remove-then-add). Projection is gated by
|
|
215
|
+
* {@link isGoalStopHookWireArmed} — storing is always safe, sending is not (yet).
|
|
216
|
+
*/
|
|
217
|
+
export function setWireSessionStopHook(prompt, opts) {
|
|
218
|
+
wireSessionStopHookPrompt = prompt && prompt.length > 0 ? prompt : null;
|
|
219
|
+
wireSessionStopHookCcSemantics = wireSessionStopHookPrompt === null ? false : opts?.ccSemantics === true;
|
|
220
|
+
}
|
|
221
|
+
/** The currently registered `/goal` Stop prompt, or null (registered ≠ projected — see the gate). */
|
|
222
|
+
export function getWireSessionStopHook() {
|
|
223
|
+
return wireSessionStopHookPrompt;
|
|
224
|
+
}
|
|
225
|
+
/** 当前这条目标登记时锁存的档位(真 = 投裸条件,假 = 投壳自造的门)。 */
|
|
226
|
+
export function getWireSessionStopHookCcSemantics() {
|
|
227
|
+
return wireSessionStopHookCcSemantics;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* 把 `/goal` 的**原始条件**包成一条对引擎 prompt 载体**可执行**的 Stop 门 prompt。
|
|
231
|
+
*
|
|
232
|
+
* 🔴 适用面(2026-07-26 提货批后):**只用于未达标线的引擎档**(见 {@link CC_STOP_SEMANTICS_MIN_SERVER})。
|
|
233
|
+
* 达标档起引擎自己按 CC
|
|
234
|
+
* 220 逐字组装(系统提示 + 会话转录 + `wrapCondition`),那一档投的是**裸条件** —— 见
|
|
235
|
+
* {@link ccStopSemanticsFromVersion} 与 {@link hooksForWire} 的分道。本函数保留,是 tolerate-absent
|
|
236
|
+
* 的实体:老引擎上退回它,仍能拦一次;删掉它等于在老引擎上静默失效。
|
|
237
|
+
*
|
|
238
|
+
* 🔴 为什么必须包(2026-07-26 live 围栏实测,不是设计偏好):CC 单进程的 prompt 钩子执行器
|
|
239
|
+
* (`src/utils/hooks/execPromptHook.ts`)会给条件**套一层系统提示**(「You are evaluating a hook…
|
|
240
|
+
* 回 `{ok:true}` / `{ok:false,reason}`」)**并把整段会话前置**,所以裸条件在 CC 那边能被真评估。
|
|
241
|
+
* 引擎侧的载体两样都没有:
|
|
242
|
+
* · 无系统提示、无会话历史;
|
|
243
|
+
* · Stop payload 的 `transcript_path` 是**空串**(server basePayload),评估者对本会话是**瞎的**;
|
|
244
|
+
* · 判据 schema 也不同 —— core 只认 stdout JSON 的 `decision:"block"`。
|
|
245
|
+
* 实测(钉版 server 1.277.1 + core 1.416 + 真模型):裸条件 `the project contains a file named
|
|
246
|
+
* DONE.txt` 送过去,载体照样被调用,模型回的是**散文**(「I don't have direct access to your file
|
|
247
|
+
* system…」)⇒ `parseHookStdout` 拿不到 decision ⇒ **一次都不拦**。即:不包 = 每次停机白烧一次
|
|
248
|
+
* 模型调用、零效果。
|
|
249
|
+
*
|
|
250
|
+
* 所以这里把条件包成引擎契约里**能落地**的形状。诚实标注它**够不到 CC 的完整语义**:
|
|
251
|
+
* · 能做到:每个 turn 的**第一次**停机尝试被拦下,目标原文重新注入给模型,逼它自查后再收尾
|
|
252
|
+
* (`stop_hook_active` 为真时门自己收手 ⇒ 不会无限拦停);真正「条件是否成立」的判断交给
|
|
253
|
+
* **有会话、有工具的主模型**,而不是让瞎的评估者假装判断;
|
|
254
|
+
* · 做不到:CC 那种「拦到条件真成立为止」(需要引擎把 transcript 交给载体)、以及「条件满足后
|
|
255
|
+
* 自动清除目标」。这两条是**等件**,meta 文案因此不照抄 CC `Pwo`(见 cmd-goal.tsx)。
|
|
256
|
+
*
|
|
257
|
+
* 台账仍存**原始条件**({@link getWireSessionStopHook}),所以 `/goal clear` 读回的是用户写的那句。
|
|
258
|
+
*/
|
|
259
|
+
export function buildGoalStopHookPrompt(condition) {
|
|
260
|
+
const reason = `[session goal] Before you stop: verify that this goal actually holds — ${condition}. ` +
|
|
261
|
+
`If it does hold, say so plainly and finish. If it does not, keep working toward it and do not ` +
|
|
262
|
+
`pause to ask the user what to do.`;
|
|
263
|
+
return ('You are the Stop gate for an autonomous coding agent session. The user set this session goal, ' +
|
|
264
|
+
'which the agent is supposed to satisfy before it stops:\n' +
|
|
265
|
+
`<goal>\n${condition}\n</goal>\n` +
|
|
266
|
+
'Stop-hook input JSON: $ARGUMENTS\n' +
|
|
267
|
+
'Reply with RAW JSON only — no prose, no explanation, no code fences.\n' +
|
|
268
|
+
'If the input JSON has "stop_hook_active" set to true, this gate already fired once for this turn: ' +
|
|
269
|
+
'reply with exactly {"continue":true} so the agent may stop.\n' +
|
|
270
|
+
'Otherwise reply with exactly:\n' +
|
|
271
|
+
JSON.stringify({ decision: 'block', reason }));
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* The merged, GOVERNANCE-GATED settings-file hooks for the wire, or undefined when none apply.
|
|
275
|
+
* Per-event arrays concatenate across sources (policy → user → project → local).
|
|
276
|
+
*/
|
|
277
|
+
export function hooksForWire() {
|
|
278
|
+
// 🔴 port 缺席 = **整条腿 fail-closed**(见文件头第三段):端没装 SettingsPort 就没有信任门,
|
|
279
|
+
// 而信任门是安全门。`hostSettings()` 自己会计 miss 并喊一次,宿主自检读得到。
|
|
280
|
+
const settings = hostSettings();
|
|
281
|
+
if (settings === undefined)
|
|
282
|
+
return undefined;
|
|
283
|
+
// trust gate first (cheapest + broadest): untrusted interactive workspace ⇒ no hooks anywhere,
|
|
284
|
+
// the engine leg included — same invariant the local executor enforces per-fire.
|
|
285
|
+
try {
|
|
286
|
+
if (settings.shouldSkipHookDueToTrust()) {
|
|
287
|
+
hostLog('debug', 'hooksWireCaps: workspace not trusted — no hooks projected to the engine');
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
/* trust check threw (early boot) — fall through; 端侧本地执行器仍 per-fire 把门 */
|
|
293
|
+
}
|
|
294
|
+
let policy = null;
|
|
295
|
+
try {
|
|
296
|
+
policy = settings.getSettingsForSource('policySettings');
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
policy = null;
|
|
300
|
+
}
|
|
301
|
+
if (policy?.disableAllHooks === true) {
|
|
302
|
+
hostLog('debug', 'hooksWireCaps: disableAllHooks (managed) — no hooks projected to the engine');
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
const managedOnly = policy?.allowManagedHooksOnly === true;
|
|
306
|
+
const sources = managedOnly
|
|
307
|
+
? ['policySettings']
|
|
308
|
+
: ['policySettings', ...EDITABLE_HOOK_SOURCES];
|
|
309
|
+
const merged = {};
|
|
310
|
+
for (const source of sources) {
|
|
311
|
+
const hooks = hooksOfSource(source);
|
|
312
|
+
if (!hooks)
|
|
313
|
+
continue;
|
|
314
|
+
for (const [event, groups] of Object.entries(hooks)) {
|
|
315
|
+
if (!Array.isArray(groups) || groups.length === 0)
|
|
316
|
+
continue;
|
|
317
|
+
// Verbatim passthrough INCLUDING malformed entries — the service is the single validator
|
|
318
|
+
// (400 fail-loud with a field path); a client-side drop would be a silent fallback.
|
|
319
|
+
merged[event] = [...(merged[event] ?? []), ...groups];
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// `/goal` 的会话级 Stop×prompt 钩子(CC 220 Xdr)——在治理门之后合并,所以 trust /
|
|
323
|
+
// disableAllHooks / allowManagedHooksOnly 任一命中时它同样不投影(managedOnly 下用户态的
|
|
324
|
+
// goal 钩子也一并禁掉,与 EDITABLE_HOOK_SOURCES 被排除同理)。默认开,见上方 overlay 注释。
|
|
325
|
+
if (wireSessionStopHookPrompt !== null && !managedOnly && isGoalStopHookWireArmed()) {
|
|
326
|
+
// 🔴 这个双 cast 是**类型面的最小且诚实的局部处置**,不是「绕过校验」:
|
|
327
|
+
// · server **运行时接受** prompt 形(hook-runner 条目白名单与 command/http/agent 并列,
|
|
328
|
+
// [1721] 6 条行为用例 + cli 仓 live 围栏双证);
|
|
329
|
+
// · 卡着的是 **SDK 类型**:`SettingsHookCommand.type` 仍是字面量 `"command"`
|
|
330
|
+
// (@sema-agent/sdk dist/settings.d.ts,prompt 形 v1 标 DEFERRED);
|
|
331
|
+
// · **registry-core 建模 prompt 形在 [1717]① 等件中**,SDK 随后放开。
|
|
332
|
+
// 到位后这里换成直类型、去掉 cast(唯一改动点,调用面不动)。
|
|
333
|
+
const goalMatcher = {
|
|
334
|
+
matcher: '',
|
|
335
|
+
// 🔴 投影形**按引擎档位分道**(锁存于 set 那一刻,见 setWireSessionStopHook):
|
|
336
|
+
// · CC 语义档(server ≥ CC_STOP_SEMANTICS_MIN_SERVER)⇒ **裸条件** —— 与 CC `Xdr` 注册的
|
|
337
|
+
// `{type:"prompt",prompt:condition}` 逐字同形;引擎自己套 CC 系统提示 + 会话转录 +
|
|
338
|
+
// `wrapCondition`,再包一层只会打架;
|
|
339
|
+
// · 老引擎档 ⇒ 包装后的可执行门 prompt(裸条件在老引擎上一次都拦不住 —— 见
|
|
340
|
+
// buildGoalStopHookPrompt 的实测说明)。
|
|
341
|
+
// 台账两档都存**原始条件**,供 `/goal clear` 读回。
|
|
342
|
+
hooks: [
|
|
343
|
+
{
|
|
344
|
+
type: 'prompt',
|
|
345
|
+
prompt: wireSessionStopHookCcSemantics
|
|
346
|
+
? wireSessionStopHookPrompt
|
|
347
|
+
: buildGoalStopHookPrompt(wireSessionStopHookPrompt),
|
|
348
|
+
},
|
|
349
|
+
],
|
|
350
|
+
};
|
|
351
|
+
merged['Stop'] = [...(merged['Stop'] ?? []), goalMatcher];
|
|
352
|
+
}
|
|
353
|
+
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
354
|
+
}
|
|
355
|
+
/** 测试钩:清 `/goal` 锁存(壳里靠进程边界隔离;包内同进程多组断言必须能清)。 */
|
|
356
|
+
export function __resetHooksWireCapsForTests() {
|
|
357
|
+
wireSessionStopHookPrompt = null;
|
|
358
|
+
wireSessionStopHookCcSemantics = false;
|
|
359
|
+
}
|