import { HooksConfig } from "@sema-agent/settings-schema/hooks"; import type { Hooks } from "@sema-agent/core"; import type { Logger } from "../observability/logger.js"; import type { HookNoticeObservation } from "../fleet/fleet-bus.js"; export declare const MAX_HOOK_ENTRIES_PER_EVENT = 32; export declare const MAX_HOOK_COMMAND_CHARS = 8192; /** 单条 hook 超时上限(秒;条目可自设 timeout,但被此值夹住——一个 86400 的 timeout 会挂死工具门)。 */ export declare const MAX_HOOK_TIMEOUT_SECONDS = 600; /** 一次 hook 事件(一次 pre/post 调用)所有同步条目的墙钟总预算(秒;32 条 × 单条 600s * 最坏可串行卡住工具门 ~320min。到点后不再起后续条目,记账降级)。 */ export declare const MAX_HOOK_EVENT_TOTAL_SECONDS = 120; /** 一次事件的 matcher 组数上限(配置广度的另一维,防组数爆炸)。 */ export declare const MAX_HOOK_MATCHER_GROUPS = 16; /** http 条目的 header 条数 / allowedEnvVars 个数上限(构造放大面)。 */ export declare const MAX_HOOK_HTTP_HEADERS = 32; /** hook 进程 stdout/stderr 各自的采集上限(字节)——防输出洪泛打爆内存/日志。 */ export declare const MAX_HOOK_OUTPUT_BYTES: number; /** * 一次事件里所有 hook 上下文片段**聚合后**喂给模型的总量上限(字符)。 * * 为什么单条帽不够:一次事件最多 {@link MAX_HOOK_ENTRIES_PER_EVENT} 条(parseHooksConfig 的广度闸), * 每条最多推两段(`decision:"block"` 的 reason + `hookSpecificOutput.additionalContext`), * {@link composeHooks} 再把部署槽与 task 槽的**成品**拼一次 —— 单条 4096 相乘后一次注入可达 ~256KB, * 而这些字符是**逐次工具调用**进模型可见文本的(core `hooks.js` 的 preToolContext / `prepare-task.js` * 的 tool_result 追加 / `runtask.js` 的 Stop follow-up),不是日志。 * * 帽值取 10_000 的依据:模型可见文本在本栈的既有预算量纲就是 10k —— core `truncateError` * (`src/core/tool-errors.ts`)的 10k 中截,逐字锚 CC 序列化器 `A7e` 的 `1e4`;[ref] 战役里 core 还要给 * `beforeToolCall` block reason 补一道同值兜底闸。取同一量级 = 「一次 hook 注入 ≤ 一份错误文本预算」, * 且 4096 的单条帽保证**前两条**钩子的话仍整段进得去(截断是配置广度的代价,不是常态)。 */ export declare const MAX_HOOK_CONTEXT_TOTAL_CHARS = 10000; export interface ParseHooksConfigResult { config?: HooksConfig; /** 人可读的第一条校验错误(HTTP 层用它 400 fail-loud;resume 防御路径用它 warn)。 */ error?: string; } /** * 把 wire 上的 untrusted `settings.hooks` 校验成契约 {@link HooksConfig} + 服务侧上限。 * 错误返回 error 字符串而不 throw——submit 路径 400 fail-loud、resume 防御路径 warn-drop,由调用方选。 */ export declare function parseHooksConfig(raw: unknown): ParseHooksConfigResult; /** CC matcher 语义:精确名、`a|b` 交替、或 JS 正则(锚定全匹配,防 "Bash" 误配 "BashOutput"); * 空/`*`/缺省匹配一切;非法正则 → 该条不匹配(fail-closed 到"不跑",由调用方记账一次)。 */ export declare function hookMatcherMatches(matcher: string | undefined, value: string): boolean | "invalid"; /** `if` 条目条件(CC-exact,对照 CC 2.1.187/198 实测行为核验的真语义,两版一致): * 值 = permission-rule 模式(`ToolName` 或 `ToolName(arg-模式)`),【不是】表达式语言——按当前工具调用 * 求值:名部走 permission matcher(交替/正则同 matcher 字段),括号内 arg-模式(`*` 通配)匹配工具输入的 * 主字符串(Bash=command,文件工具=file_path/path)。CC 行为逐条目:不匹配 → 跳过该条目(记日志); * 非工具事件(payload 无 tool_name)→ "cannot be evaluated" 跳过;解析失败 → 跳过。返回: * true=跑 / "not_matched" / "unevaluable"(调用方分别记账;跳过永不静默)。 * * 2.1.250 复核([ref],2026-08-30;语料=sema-internal `blueprints/cc-decoded/cli250.js`):求值器与 220 * 逐字同构——五种工具事件之外「cannot be evaluated for non-tool event」跳过、「Skipping hook due to if * condition … not matching」跳过,两串日志逐字在场;括号内容经工具 `preparePermissionMatcher` 求值,与 * 本实现同向。差异登记(不改行为):①250 证据里名部=**规范化后等值比对**(`vd(toolName)!==o`), * 「交替/正则」是 matcher 字段的文法——本实现名部走 hookMatcherMatches 属**超集**(alternation 也认), * 如实登记不收窄;②250 的 `if` 已扩到 prompt/agent/http/mcp_tool 条目(本 runner 只翻 command,不涉)。 */ export declare function hookIfMatches(cond: string, payload: unknown): true | "not_matched" | "unevaluable"; /** createTaskHooks 的装配上下文(payload 的 CC-verbatim 字段来源)。 */ export interface HookRunnerContext { logger: Logger; sessionId: string; /** hook 命令的工作目录 + payload `cwd` + `CLAUDE_PROJECT_DIR`(host lane 的 session cwd;无则 server cwd)。 */ cwd: string; permissionMode?: string; /** 请求自带的 `settings.env`(单用户闸后)——hook 进程 env 与 http $NAME 插值的用户变量通道。 */ shellEnv?: Record; /** 阶段三b:`prompt` 条目的模型调用载体(部署组装:一次非流式 completion,费用不折 task budget)。 * 缺席 ⇒ prompt 条目跳过记账(部署没配模型面就诚实降级,不瞎猜)。fake 可注入 ⇒ hook-runner 保持单测性。 */ hookLlm?: HookLlmCall; /** 阶段三b:`agent` 条目的子代理载体(部署组装:runTask 读-only 手 + 可重入禁 + 独立小 budget)。 */ hookAgent?: HookLlmCall; /** hook 观测回调(纯 observe;部署把它接到 fleet 流)。**两族**: * · `hook_decision_unavailable` —— 一次判定**未能完成**。方向可以 fail-open,但**不能连 * 「我这轮没看住」都不说** —— 否则模型拿到的放行与「已达成」无法区分,而产品刚对它承诺过 * 「没达成不许停」; * · `hook_non_blocking_failure`([ref])—— **hook 自己坏了**(exit≠0∧≠2 / 超时 / 起不来 / 坏 JSON)。 * 裁决面按 [ref] 裁量① 维持 CC parity 不动,坏掉这件事本身必须让人看得见。 * ⚠️ 它**必须**是不改变运行的通道:`additionalContext` 不行(那个会让这一轮不结束)。缺席 ⇒ 只进日志。 * ⚠️ 回调**抛错不许误伤任务**(见 {@link emitHookNotice}):观察面绝不成为新的失败源。 */ onHookNotice?: (n: HookNoticeObservation) => void; /** Stop × `type:"prompt"` 是否把**会话 transcript** 交给评估者(CC `execPromptHook` 形)。 * 缺省(undefined)= **开**。`false` ⇒ 整条路不启用(prompt 条目在 Stop 上被跳过并记账)—— * 部署侧的显式逃生口(`SEMA_STOP_PROMPT_TRANSCRIPT=0`)。见 `hooks.stop` 里那段三问。 */ stopPromptTranscript?: boolean; /** asyncRewake wake 管道(契约:Background + wake the model on exit 2)。部署组装:live 腿=同 session * 的 TaskStream.steer(下个 turn 边界注入);返回 false=没有活流可投(任务已结束/挂起/别副本)—— * 调用方记账不重试(挂起腿的 pending-steer park 是后续切片)。缺席 ⇒ asyncRewake 降级纯 async(记账)。 */ wake?: (text: string) => Promise; } /** 阶段三b 模型调用载体契约(prompt=直连一次 completion;agent=runTask 收敛后的 final text)。 * 返回 ok/text 或 ok:false/error——载体自己兜超时(timeoutMs 是硬顶,载体可更严),绝不 throw。 */ export type HookLlmCall = (opts: { prompt: string; model?: string; timeoutMs: number; /** 本次调用需要的**输出**上限(tokens)。缺席 ⇒ 载体用它自己的缺省(1024,为 titler 那类"一行标题" * 的廉价判定调好的)。 * 🔴 **加这个位是因为共用缺省真的咬人了**:CC 的条件评估者被提示词**明确要求**「quote evidence from * the transcript」,推理档模型的 thinking 还与输出**共用**这份额度 ⇒ 1024 会把判词截断、 * 甚至一个字都吐不出来 ⇒ 判词整条被丢弃 ⇒ **fail-open(该拦没拦)**。 * ⇒ 判据:**给一个载体加新消费者时,要重访它为旧工况调好的每一个参数** —— * 那些参数的旁注里通常写着一句"永远用不到更多",而那句话是**对旧工况**说的。 */ maxOutputTokens?: number; /** 系统提示。CC 的 `execPromptHook` 给条件评估者套一段(见 `cc-stop-prompt.ts` 的逐字移植)—— * anthropic 形走 Messages API 的**顶层 `system` 字段**,openai 形走**前置的一条 system 消息**, * 两边同一语义。缺席 ⇒ 与此前逐位相同(无 system)。 */ system?: string; }) => Promise<{ ok: true; text: string; } | { ok: false; error: string; code?: HookLlmFailureCode; }>; /** 失败判别码(B8:判别一律走码,禁按 error 文案分支——v3.1 批2,统检第二波 high)。error 仍是给人看的 * 自由文本;code 是给控制流的。缺席=未分类失败(不重试、不特判)。 */ export type HookLlmFailureCode = "no_content" | "hard_timeout"; /** * hook 上下文片段 → 喂给模型的单串。**所有** `additionalContext` 聚合点的唯一属主(此前是同一个 * `contexts.join("\n")` 表达式抄在二十余处,总量帽无处可挂 —— 那正是漂移成因)。 * * 三条语义,都是承重的: * 1. **顺序保留、只截尾**:先来的钩子先说话;一旦装不下就停,后面的条目一律不再挤进来(哪怕更短)。 * 按长度重排会让"第 3 条钩子的话"随别人的长度忽隐忽现,排障时无从复现。 * 2. **截断可见**:尾巴挂一行标记说明省了几条、丢了多少字符。静默丢弃会让 hook 作者以为自己的 * context 生效了,而模型那头根本没见过 —— 这类"以为配好了"的缺席比长文本本身更贵。 * 3. **标记不计预算**:与 core `truncateError` / CC `A7e` 同姿势(marker 不占那 10k),否则帽值的 * 含义会随标记文案长度漂。 */ export declare function buildHookContext(parts: readonly string[]): string | undefined; /** * 阶段三a:`http` 条目——契约语义 = POST hook 输入 JSON 到 `url`;headers 里的 `$NAME` 仅当 NAME 列在 * `allowedEnvVars` 才从 worker 进程 env 插值(配置本身绝不携带密钥值,契约同边界)。 * * 结果折算成伪 {@link CommandRunResult},让 9 事件的既有折叠逻辑零改动复用: * - 2xx + JSON body → 同 exit-0-stdout-JSON(SyncHookOutput 决策面)。⚠️ 这是对 CC 行为的合理近似 * (契约只定"POST 输入";端点回非 JSON → 忽略,与 command 同姿势),板上已请壳确认 CC-exact。 * - 非 2xx → code=HTTP status(≥100,永远不会撞 exit-2 的阻断语义)→ 非阻断 warn 记账。 * - 超时/网络错 → timedOut / spawnError 同款,非阻断。 * 🔒 redirect:"error"——插值出的 Authorization 头绝不跟着重定向跨源;URL 指向哪由用户配置负责 * (hook 跑在单用户 host lane=用户自己机器,与 command 条目跑任意 sh 同一 class ② 信任面)。 */ /** * 阶段三b:跑一条 `prompt`/`agent` 条目(design/HOOKS-PHASE3B-PROMPT-AGENT.md)。两者共此函数—— * 差异只在载体(prompt=ctx.hookLlm 直连一次 completion;agent=ctx.hookAgent 读-only 子代理)与默认 * 超时。`$ARGUMENTS` 占位符替换为 hook 输入 JSON(CC 语义;与 command stdin 同一份 payload,同 * 256K 截断纪律),模型输出折成伪 CommandRunResult{code:0, stdout:text} → 每个事件既有的 * `parseHookStdout` 决策面(SyncHookOutput)零改动继承:输出可解析 JSON=决策(allow/deny/ask/ * updatedInput/continue:false…),非 JSON=该事件对 plain text 的既有语义(与 command exit-0 同)。 * 载体失败/超时 → spawnError/timedOut 同款非阻断。可重入禁是【载体】的责任(部署组装的 runTask * 不装 hooks——结构性,见 main.ts);hook-runner 自身不发模型调用,fake 载体即可单测。 */ /** CC 评估者的额外输入(prompt 臂=Stop×prompt,见 `cc-stop-prompt.ts` / `branch-transcript.ts`; * agent 臂=每事件,见 `cc-agent-hook-prompt.ts`,[ref] P2-5)。 */ export interface LlmHookExtra { /** CC 逐字的系统提示。 */ system: string; /** 触发事件名——no_content 重试的 warn/notice 记账用(此前硬编 "Stop",扩 agent 臂后必须真名)。 */ event: string; /** **会话 transcript,放在条件之前** —— CC 的包装句写的是 "transcript **above**", * 所以它必须真的在上文,否则那句话本身就是在骗模型。仅 prompt 臂(Stop×prompt)供给; * agent 臂缺席=登记偏离(CC 给的是转录**文件路径**,本仓 agent 载体无该设施, * 见 cc-agent-hook-prompt.ts 头注)。 */ transcript?: string; } /** * 把校验过的 {@link HooksConfig} 翻成 core `Hooks` 回调(阶段二:engine-owned 9 事件全点亮)。 * 没有可点亮的条目 → undefined(spec 不挂 hooks,deps 路径原样)。 */ export declare function createTaskHooks(config: HooksConfig, ctx: HookRunnerContext): Hooks | undefined; /** * 把部署级 hooks 基线(deps.hooks,如 TOOL_TRACE 观测)折进 task 级 hooks——core 是整槽覆盖 * (`spec.hooks ?? deps.hooks`),不折叠就静默 shadow 部署观测(runtask gateBaseline 注释点名的坑)。 * 语义:部署槽先跑(观测看到原始输入/输出),task 槽后跑;结果合并: * - preToolUse:deny > ask > allow 折叠;updatedInput 串行线程(部署重写喂给 task 槽);context 拼接。 * - postToolUse/postToolUseFailure/postToolBatch:双跑,additionalContext 拼接,updatedOutput 取 * 后者(task 槽)优先。 * - userPromptSubmit/stop:部署 block 优先短路(不再跑 task 槽),否则双跑、context 拼接、task block 生效。 * - preCompact:双跑,block 部署优先,additionalInstructions 拼接。 * - stopFailure/postCompact(observe-only void):顺序双跑,无返回可折。 * 单在则透传。 */ export declare function composeHooks(deployment: Hooks | undefined, task: Hooks): Hooks; //# sourceMappingURL=hook-runner.d.ts.map