import type { IncomingMessage } from "node:http"; import { type AskRequest, type AskOutcome, type ReadRootGrantCandidate, type RuleOffer } from "@sema-agent/core"; import { type CardRulePersisted, type RuleConsentLane } from "./rules-consent.js"; import { type DenialLimitFallback, type RuleOffersAbsence, type ApprovalRequestFrame, type ApprovalRevokeFrame, type RuleEvidenceProjection } from "./approval-card.js"; import { type ApprovalAskStore } from "./plugins/approval-ask-store-sql.js"; import type { ApprovalAskAuditSink } from "./approval-ask-audit-store.js"; import { type GovernanceAskMarks } from "./governance-ask-marks.js"; /** [ref]:审批 gate kind 闭集(core gateMatch 的 `human`/`irreversible_ask` ↔ outcome.gate "policy_ask")。 * 此前 7 份手写副本散在两只 checkpoint store 的数组/SQL 字面与 /decide 守卫——core 加审批味 kind 时 * 全部静默漂移。core 无导出词表(全大写导出面零命中,2026-08-09 亲验),属主落此;SQL IN 片段从 * 数组派生保证同源。门=test/approval-gate-kinds-single-owner.test.ts(副本回潮即红)。 */ export declare const APPROVAL_GATE_KINDS: readonly ["human", "irreversible_ask"]; export type ApprovalGateKind = (typeof APPROVAL_GATE_KINDS)[number]; export declare function isApprovalGateKind(k: string | undefined): k is ApprovalGateKind; /** 两方言同形的 SQL IN 片段(值为闭集常量字面,无注入面)。 */ export declare const APPROVAL_GATE_KINDS_SQL_IN: string; /** * 🔴 [ref](案A)追记 F6 —— durable 回决口(`POST /v1/tasks/:taskId/asks/:askId/decision`)的 **200 体 * additive 判别位**:这次决议在**本副本**上有没有被一条活着的腿消费掉。 * * 值只有一个词(`"absent"`),因为只有 absent 这一面需要披露:在场 = 「行判了,但本机没有任何东西 * 因此续跑」。反面(真被消费)不铸键 —— additive 只记真,与 `updatedInputForwarded`/`decisionNote` * 的先例逐字同族。**缺席禁读作「有消费者」的证明**:老版本 server 从不发这个键。 * * 成因两形([ref] 起;[ref] 复审件3 曾补的第三形 = 弃单墓碑守卫翻 park,随守卫整删退役。进程内仍可 * 短暂出现「腿在场而零交付」:编辑在飞让路 —— 标记放开后的下一拍按行兑现):①持有悬挂 promise 的是 * **另一个副本**;②本副本重启后本地窗已空。 * 两形的处置同一句:壳读 run 面判续跑没有,绝不把 200 无键当成「run 已续跑」的证明。 * * 为什么不是 409/410(施工时的二选一,裁定写在这里):409/410 说的是「你的请求没有生效」,而这次决议 * **确实生效了**(行是 DECIDED,幂等回放读得到、审计面记得住)——把它渲成错误会让壳把一次成功的 * 人类决议当成失败去重试,而重试只会撞回幂等回放。200 + 机读披露是唯一同时诚实的形。 * * 语义边界(禁扩读):它答的是「本副本此刻有没有活体消费者」,**不是**「这条 run 还活着吗」——持有 * 悬挂 promise 的可能是另一个副本(F4 的轮询腿会在它自己的下一拍续跑)。run 的生死读 run 面。 */ export declare const ASK_DECISION_CONSUMER_ABSENT = "absent"; /** * [ref] §三 PARKED 臂:把一条已 park 的 ask 的决议交给**既有赎回腿**所需的全部材料。 * * 🔴 契约锚([ref] 锚①):本席是「同腿新调用方,零新终局语义」—— 决议必须走 checkpoint 的 decide * CAS(单赢者),席的实现**绝不**在 ask 行上另开第二个终局写点。gate 三坐标是 `bindBatch` 从 * checkpoint 抄回行上的那一份,server 永不重算(绑定校验归 core 的 resume 侧,与 `/decide` 腿同姿势)。 */ export interface ParkedAskRedeemRequest { /** 店内主键(日志/回执锚;赎回判定用的是下面三坐标)。 */ askId: string; /** PARKED 坐标三件(`AskRow.gateToken/gateBoundCallId/gateBoundInputHash`)。 */ gateToken: string; gateBoundCallId: string | null; gateBoundInputHash: string | null; /** wire 三选一映行上的二值(`allow`/`allow_session` ⇒ `approve`)。 */ decision: "approve" | "deny"; /** [ref] 同源的人写理由(deny 时 core 把它交给模型;allow 只进审计面)。 */ note?: string; /** [ref] ctrl+g 编辑放行的实参整体替换(approve 才有意义)。 */ updatedInput?: unknown; /** 发起本次 respond 的**已验证**调用方(live 腿 `gatedPrincipal` 的那一把)。席在赎回之前拿它与 * **checkpoint 当下的 scope** 对一次(codex R1-[high] 一:收编改写身份之后,任何缓存的属主判据都 * 过期;门必须贴在当下的持久事实上)。 */ principal?: string; /** 发起本次 respond 的真实 HTTP 请求。赎回腿据它做准入解析与舰队 scope(与 `/decide` 腿同姿势); * 缺席 ⇒ 那条腿按「无请求」形处置(它自己的成文语义,本席不替它决定)。 */ httpReq?: IncomingMessage; } /** [ref]:赎回席(装配层注入;缺席 ⇒ PARKED 臂逐字回落修前 404)。回的是赎回腿的原始 HTTP 结果面。 */ export type ParkedAskRedeem = (req: ParkedAskRedeemRequest) => Promise<{ status: number; body: Record; }>; /** A live approval frame delivered to whoever tails this run's stream. `type` IS the SSE event name (named-event * convention, same as question). The shell renders `tool_approval` as the CC three-choice card and dismisses on * `tool_approval_complete`. */ export interface ToolApprovalFrame { type: "tool_approval" | "tool_approval_complete"; approvalId: string; /** "tool_approval" only: which tool asked (canonical core name as core sent it) + the policy's ask message. */ toolName?: string; /** [ref] "tool_approval" only: **core 的 tool-call id**(`AskRequest.toolCallId`,逐字透传)——消费端用它 * 把审批卡锚到助手消息里那个 `tool_use` 块。`approvalId` 是审批自己的新 uuidv7、与该块**无关**, * 此前帧上只有它 ⇒ 两个宿主都只能拿它当 key 铸卡,而那个 id 之后再没有任何帧提到过,卡永远停在 * awaiting_approval。「拿最近一次 tool_start 当锚」这类补偿也不成立:**审批帧比 `tool_start` 先到** * (cli 帧级取证 4020ms vs 4142ms)。durable(parked)腿本来就存着这个值(parked-decide.ts), * 本键 = 把 live 腿的字段集与它对齐。闭合帧(`tool_approval_complete`)不带——它按 approvalId 消卡, * 多一个锚只会多一份歧义。 */ toolCallId?: string; /** **只在委派子代的 ask 上在场**(现判别键 = {@link ToolApprovalFrame.fromSubagent};本字段本身 * [ref] MED-1 后不再是判别式,core JSDoc 钉明「不是子代判别键」)。值 = 子代的 core session id * (与 task_progress.taskId / bg_notification.sessionId 同值域,可关联子代行)。旧壳(< fromSubagent * 接线)兜底按在场性渲染归属标仍然正确。 */ sourceTaskId?: string; /** [ref] [ref]②:core 1.378 显式判别键——委派子代 gate 发起的 ask 恒带 `true`,受信 internals 事实 * (worker 不可自造/不可抑制)。取代 [ref] MED-1 的「在场 ∧ ≠ 宿主 sessionId」权宜判别式。 */ fromSubagent?: true; /** [ref] 展示身份(子代 agent 名)——UNTRUSTED-for-display(core 未做秘密脱敏)→ redactSecrets 后渲。 * 仅子代 ask 上可能在场(core 未必总能解析出名字)。 */ sourceAgentName?: string; /** core 5.9.0 W1([ref]):审批上浮的出处链——{parentToolCallId, depth, agentName?}(孙代最内层帧 * 胜出)。只读展示增强(cli [ref] 验收形=审批卡带子代出处);agentName UNTRUSTED → redactSecrets。 * 只在子代 ask 上在场(与 fromSubagent 同门)。 */ delegation?: { parentToolCallId: string; depth: number; agentName?: string; }; message?: string; /** * [ref]/[ref] **ADDITIVE**,`"tool_approval"` only —— `true` ⇔ 这只 ask 的门来自**运维治理层** * (`AUTONOMY` / `commandPolicy` / `MANUAL_MODE_SHELL_GATE` / `SENSITIVE_WRITE_PATTERNS` 合成的那条 * policy,`applyRuntimeGovernance`),而不是模型默认门或客户端表态。壳据此渲染「治理强制」徽标,回答 * [ref]#1/[ref] 实证的那个 UX 缺口:「我都开 bypassPermissions 了为什么还在问」。 * * 🔴 **缺席 ≠ `false`**:本键**只在为真时在场**,缺席的含义是「没有治理来源的证据」——既覆盖真正的 * 非治理 ask(如 `APPROVAL_REQUIRE` 显式列名),也覆盖判据够不着的形(如治理层 shellGate 停在 * `"classify"` 档时,一次 shell ask 究竟出自分类器还是别的门,在 `AskRequest` 上无从分辨)。 * 消费端**禁**把缺席读成「这不是治理门」。判定缝的真形与为什么不能走 `decisionReason`: * `governance-ask-marks.ts` 顶注。 */ governanceForced?: true; /** [ref]([ref]③/[ref];**ADDITIVE**,`"tool_approval"` only)——core PreToolUse hook/策略族铸的 * **安全类 ask 出身位**(`AskRequest.requiresRealApproval`,core 5.37 起可选、真才带)。帧顶层契约与 * core 侧同构:**真才带,缺席绝不编 false**(governanceForced 同形)——与 `card.requiresRealApproval` * (approval-card.ts,恒在布尔)**两侧契约刻意不同、别混**:卡是 durable 店形(askStore 在场)专属, * 本键让 live-only 部署(店缺席、帧无卡)也能读到出身位;安全类标记不得依赖店在场([ref] 结构性 * 缺口成文)。消费语义(cli 挂点已预埋):在场 ⇒ 一切自动放行让位(含记住的规则与 bypass 姿态)。 */ requiresRealApproval?: true; /** * **[ref]**(core 7.26.0;**ADDITIVE**,`"tool_approval"` only)—— **这一问没有任何规则能清掉** * (`AskRequest.mandated`:org / 治理层、保护判官、hook 的筛查权、被祖先的应答者裁决的问…… 由引擎在闸站 * 盖章,策略方不可自填、只可收紧)。壳据它渲染「规则清不掉这一问」,并让**声明了自动应答姿态**的席位知道 * 哪些卡它不该自动答。 * * 🔴 **别拿 `ruleOffersAbsence:"mandated"` 顶替它**(core 头注逐字警告过这条路):那只键回答的是**规则报价 * 车道**有什么可给,而那条车道对它不说话的工具、**没接规则店的部署**(本仓的 live-only 形)、装不下规则的 * 任务都是结构性沉默 —— 它的缺席**不是**断言。本键是这一问**自己**的属性,凡强制皆在场。读错的代价 core * 实测过:同一条根外读命令在 shell 工具上是强制问、在 monitor 工具上看起来是普通问。 * * 🔴 **它不是 `requiresRealApproval`**:强制只说「没有规则能清掉」,**不**说「没有应答者可以答」—— * 一个人、一只 hook、一张一揽子 `onAsk:"allow"` 席照常答得了强制卡。两键别互推。 * * 🔴 **真才带,缺席绝不编 `false`**(`governanceForced` / `requiresRealApproval` 同形)。缺席同时覆盖 * 「不是强制」「老引擎」「这是一张从 park 行上重铸的耐久卡」三形 —— **耐久面上本仓不造孪生座**:core 的 * 寄存行(`PendingAction`)没有这一位(KL-997a 亲读登记),行上强制表现为 `ruleOffersAbsence:"mandated"` * 且无 `ruleOffers`;在 `card_json` 侧自铸一个就是本仓成了这个事实的第二个源(而 park 那侧根本没有输入)。 * 🔴 echo-only:server 不据它做任何裁决(强制的判定属主在 core 的闸站)。 */ mandated?: true; /** * **S-125③/[ref]**(core 7.5.0;**ADDITIVE**,`"tool_approval"` only)—— 这只 ask 的**出身**: * 谁提的这一问,一个 core 闭集词(`ASK_ORIGINS` **11 员**,core 7.14.0 现值:content_question / * ancestor_marked / org_unavailable / org_rule / rule_store_unavailable / hook / ask_rule / * denial_limit_fallback / shell_gate_tighten / safety_tighten / policy)。引擎在门上盖章(core d.ts: * 「engine-stamped at the gate, never a policy's claim」),`AskRequest.origin` 逐字透传。 * * ⚠️ **core 7.14.0 [ref] C2 BREAKING**:`unresolvable` → `ancestor_marked`,**无别名**。server 侧零形变 * (本键一直是 `string` 逐字透传),但**消费端的 switch 要改词**;7.14.0 之前 park 下来、行上存着 * `unresolvable` 的老记录,在 core 每一道 `isAskOrigin` 筛上自此都是非成员(投影处不回显 ⇒ 本键缺席), * 而那一行的 `org` 事实不受影响。 * * 🔴 **echo-only,零消费**:自动车道的资格判据全在 core(`classifierMayAnswer` 的闭表),server * **不许**据本键自铸第二张资格表 —— 那是同一语义面两个写者(源头修复纪律),与 * `denialLimitFallback` 顶注那条「不据 autoDenyAfterMs 自铸第二只定时器」同一条禁令。 * * 🔴 **不枚举词表**(`string` 而不是本仓自铸的联合):词表单一属主在 core,在这里抄一份会把 core 加的 * 新词吞成缺席 —— 与 `wiring_manifest.autoMode.reason` 的透传纪律逐字同规。缺席 = 老引擎 / 非 ask 路径。 * * ⚠️ 与 {@link governanceForced} **不是**一回事,别互相顶替:本键答「哪一类权威提的问」,那一键答 * 「**本部署运维治理层**是不是这只 ask 的门」。`origin: "policy"` 覆盖的是「任何部署 ToolPolicy 的 * ask」,治理策略产的与普通策略产的在这一个词上同形 ⇒ 顶替不了(逐字论证见 `governance-ask-marks.ts` 顶注)。 */ origin?: string; /** * **[ref] C3**(core 7.14.0;**ADDITIVE**,`"tool_approval"` only)—— `origin: "rule_store_unavailable"` * 的**机制判别位**:持久规则车道这一次**读不下来的是哪一半**。core 闭二词 * (`RULE_STORE_UNREADABLE_KINDS`):`"store"` = 接上的规则店读不了(读失败 / 超时);`"call"` = 店读到了, * 但**这条命令**按人的 deny/ask 行读不出来(收紧 lexer 的 `unreadable`:展开里含规则词、引号没闭、语法 * 错——具体那句在 `message` 里)。`AskRequest.ruleStoreUnreadable` 逐字透传。 * * 🔴 **在场 ⇔ `origin === "rule_store_unavailable"`**(core 顶注逐字)。两词对审批人是两件事:`store` * 指向「去看规则店 / 网络」,`call` 指向「这条命令的写法让规则判不了」——一个 origin 词对这两件事同形。 * 缺席 = 车道把店和这条命令都读通了,**或**本部署没接规则车道;消费端禁把缺席读成对规则店健康的断言。 * * 🔴 **不枚举词表**(值形是 `string` 而不是本仓自铸的联合):词表单一属主在 core * (`isRuleStoreUnreadable` 是它的成员判),抄一份会把 core 加的新词吞成缺席 —— 与 {@link origin} 逐字同规。 * * 🔴 **echo-only,零消费**:策略自声明的值在 core 的**唯一**盖章点就被抹掉了,server 不重算、不再筛第二遍, * 更不许据本键自铸第二张车道资格表 —— 同 `origin` / `denialLimitFallback` 两条顶注的同一条禁令。 * * ⚰️ 本键的前任 `classifierUnavailable`(S-185 / [ref])随 core 7.14.0 [ref] **整族退役**:引擎那份事实改骑 * `GateDisposition.denied.cause`(`tool_end.gate` / `permissionDenied.gate`),ask 面自此没有这一位。 */ ruleStoreUnreadable?: string; /** * **[ref]**(core 7.19.0;**ADDITIVE**,`"tool_approval"` only)—— 这只**根外读** ask 的**清障目录**: * 把 `dir` 加进本会话的读目录(壳侧既有的 `/add-dir` 口),同一条调用就不会再问。 * * 🔴 **在场才读,永远不读缺席**。缺席不是「无计可施」:它同时覆盖敏感路径 deny 行(按模式判,加任何 * 根都不动它)、读边界压根读不懂的命令(heredoc / 换行 / 命令替换 / 参数当程序)、没展开的 glob、 * 递归遍历、以及每一只**不是**读边界提的 ask —— 7.18.0 及以前的**每**一只 ask 都是这一形。 * * 🔴 **原样回传**(core 契约 `shell.read_boundary.grant_candidate_is_the_grant`):`dir` 是绝对、已做词法 * 折叠、用读边界自己的拼法写的串;**显示的串就是要加的串**。壳可以为了好看渲一个缩短形,回传必须是 * 本键里的原串。⇒ 本仓的姿势是「整只逐字带,或整只丢」,**绝不截**(界见 core 的 `READ_ROOT_CANDIDATE_DIR_MAX`)。 * * 🔴 **echo-only,零判读**:server 不从命令文本重推目录,也不复核它真不真能清掉这只 ask —— core 已经 * 把提议的根放回读边界又走了一遍才递出来,下游再判一遍就是同一事实的第二个判官(与 `origin` / * `ruleStoreUnreadable` / `denialLimitFallback` 三条顶注的同一条禁令)。 * * 🔴 **只此一面**:core 的负控逐字写着「the durable park row has no twin of this seat in this version」—— * 一只 PARKED 的根外读 ask 渲的仍是单独那张确认卡。所以 `card_json` / 耐久面**没有**这一座,本仓也不 * 自铸一个(自铸 = 同一事实的第二个源,而且那个源会在 park 兑现时说谎)。 */ readRootCandidate?: ReadRootGrantCandidate; /** * **E-14**([ref]② Trojan Source 族;**ADDITIVE**,`"tool_approval"` only)—— 这只 ask 的**工具输入** * (`AskRequest.args`)里含 bidi 控制符(LRM/RLM、嵌入/覆写、隔离符;字符类属主 `text-bidi.ts`)。 * * 🔴 **真才带,缺席绝不编 `false`**(与 `governanceForced`/`requiresRealApproval` 同形)。缺席的含义是 * 「没扫到」——它同时覆盖「真的没有」与「args 序列化不了(循环引用)所以扫不了」两形,消费端**禁**把 * 缺席读成「已确认干净」。 * * 🔴 **披露位,不是清洗位;字节零改**。审批卡是本仓唯一一处把模型写的命令交给**人眼**判断的面,而 * bidi 覆写让人眼读到的顺序与真正执行的字节顺序不同 —— 人批准的是 A、跑起来的是 B。清洗会改掉即将被 * 执行的那串字节(卡上显示的与真跑的不是同一个东西),**比不披露更坏**;所以 server 只报「有」,显形 * 归壳的渲染面(转义/高亮/加标记,壳自己定)。 * * **判据素材** = `boundArgs` 里对**原字节**(`redactDeep` 之前)的那一次序列化,且在**字节帽判定之前**算: * · 超帽时 args 以 `argsOmitted:true` 缺席上帧(卡上根本没有 args)—— 那正是人最看不见输入的一格, * 更需要知道「你看不到的那份里有隐形字符」,所以帽不影响本键; * · 序列化失败(循环引用 / 抛错的 `toJSON` / BigInt)⇒ 两件事一起交白卷:args 不上帧,本键**不铸** * (两次 `JSON.stringify` 共用同一个 try/catch —— 顶注反对的是第二个**静默降级点**,不是第二次序列化); * · 🔴 **7.70.0(S-103)改素材**:此前用的是脱敏后那份(理由=与卡面同源),而本版 `redactSecrets` 入口 * 先剥全部 `\p{Cf}` —— bidi 控制符**整族都是** `\p{Cf}` ⇒ 在脱敏后的文本上扫,本键恒缺席、E-14 整条 * 失效(六格红先复现)。所以判据素材改回原字节;同源那条理由让位于「信号必须真」。 * · ⚠️ **上面「字节零改」那一条自 7.70.0 起对 `\p{Cf}` 族不再成立**:帧/卡上的 args 是**剥后**字节 * (与 core 显示面 `renderUntrustedCommandText` 同律,显示的是真正执行的逻辑顺序),壳因此**拿不到 * 「在哪一位」**、只能按整只 args 打标。这是有意的取舍(夹层密钥的根治要求单一剥点),不是遗漏; * 要恢复定位面需要给脱敏器一条 span/偏移表回填路(S-103 定案明确不做),不是在这条腿上加特判。 * * 判据**不看 `message`**(引擎/策略写的说明文本,不是待执行输入):本键被壳读作「**待执行的输入**可能 * 在骗你的眼睛」,把两个来源折进一个布尔会让壳无法判断该给哪一段加显形标记。 */ inputHasBidi?: true; /** [ref]([ref]② cli 请托;**ADDITIVE**,`"tool_approval"` only,7.34.0+)——窗三键,与 * `approval_request` 帧([ref] 呈卡帧)同名同义:`expiresAtMs` = 本 ask 的绝对到期墙钟 * (有店=行上一次铸定的 `expiresAtMs`,与卡帧/落库同值;无店 D1=注册时刻算定的同名初值), * `serverNowMs` = **帧铸造时刻**(cli [ref] 锚定语义:倒计时锚=帧到手时刻的服务器钟), * `expiresInMs` = `max(0, expiresAtMs - serverNowMs)` 现算。三键同生同缺(windowZero 腿不发帧, * 发出的帧恒带);旧消费端无感,新壳借此给旧族帧补倒计时([ref] Q2 那类 77 分钟死卡的根治半场)。 */ expiresAtMs?: number; expiresInMs?: number; serverNowMs?: number; /** * [ref](core 5.25.0,[ref] 接力契约 / [ref] 主件;**ADDITIVE**,`"tool_approval"` only)—— * 这只 ask **命中了**调用方的一条持久 allow 规则,而那条规则**没能清掉它**。值 = 被越级的那条规则 * **原文**(core `AskRequest.persistedRuleShadowed`,四个 mint 点全带;core 侧已过 `inlineUntrusted`)。 * * 🔴 **它存在的理由是一次真实的用户面回归**:core 5.25.0 收窄了消音边界(裁定逐字:「Allow rules * silence the CLASSIFIER's questions, never a MANDATED one」)—— `shellGate:"always"` 的部署、工具自带 * egress/irreversible mark、以及既有的 governance 三类门下,用户此前被规则消掉的 ask **重新出现**。 * 没有这个键,人看到的是「我明明点过『不再询问』,它怎么又问」,唯一合理的结论是「我的规则坏了/没存上」。 * 有了它,壳能渲「你的规则仍在,只是这次调用被(运维令 / 工具本性)要求逐次确认」。 * * 🔴 **缺席 ≠「你没有规则」**:本键只在「有规则命中 ∧ 规则清不掉这只 ask」时在场。绝大多数 ask * 压根没有规则命中(缺席),而**命中且清掉了**的那些根本不会变成 ask(它们被消音了,没有卡)。 * 消费端禁把缺席读成「你在这条命令上没有规则」。 * * 与 {@link ToolApprovalFrame.governanceForced} **刻意分列、不合并**:那个键回答「门是谁下的」 * (运维治理层),本键回答「你那条规则怎么了」。governance 只是不可消音的三个来源之一,另两个 * (doctrine `always` / 工具自带 mark)不打 governance 标 —— 合并会让后两类的 ask 要么谎报治理出身、 * 要么丢掉规则解释。出身的完整推法见 [ref]:`gate.safetyAxis` + `riskDescriptor.shellGateDoctrine` + * `realApproval.origin` 组合读,server 不新铸出身键。 * * 值是**用户内容族**(规则原文是人写的文本)⇒ 与 `message`/`sourceAgentName` 同待遇:`redactSecrets` * 后上帧,UNTRUSTED-for-display。空串**不铸键**(core 只在真有命中时带它,空串是坏值不是「空规则」)。 * * 耐久路(park 行)的对偶是 `gate.riskDescriptor.shadowedRule` —— 我方对 `riskDescriptor` 是**整体透传** * (SQL 双生落整只 JSON 列、`listPending` 整只回读、两条 durable 读面整行上 wire),所以那一路**键集**零施工; * 唯一的施工是**内容**:那两条读面(`GET /v1/approvals` + `/v1/approvals/stream`)在读边界对这一格补 * `redactSecrets`(core 只中和不脱敏,而运维队列是跨租户可见的那一条 —— 见 * `http/routes/approvals-assistant.ts` 的 `redactPendingDisclosures` 顶注)。 * 两路的钉见 `test/approval-shadowed-rule-wire.test.ts` 与 db-integration 的「[ref] 件4」格。 */ persistedRuleShadowed?: string; /** * [ref] 车二(core 5.18.0 [ref]):`"tool_approval"` only —— 引擎为这次 ask 铸的**规则候选** * (`AskRequest.ruleOffers`,core 5.58.0 判别联合逐字透传:闭词表 `kind`/`match` + 引擎铸的文本, * server 不重铸不重排;序即契约——exact single 恒 0、batch 恒末,选择键=offer index)。 * 壳据它渲「不再询问」,回决时用 `persistRule.rule` 报出选中的那一条。 * * 🔴 **在场性即承诺**:只在**规则店真装配**(协调器拿到 `ruleConsent`,与 core 的 * `RunnerDeps.permissionRuleStore` 同源于 main.ts 的同一个对象 ⇒ 与 manifest 的 * `permissionRules.storeWired` 不可能相左)时在场。店缺席仍投 = 一格按下去无处可兑的「不再询问」, * 是 wire 谎言(判据逐字见 `trace/core-keyset-guard.ts` ④ 面本键那一段)。 */ ruleOffers?: readonly RuleOffer[]; /** * [ref] 第五单(core [ref] 修②;**ADDITIVE**,`"tool_approval"` only)—— `ruleOffers` 的**缺席因由**: * 规则车道在场却无可给时,引擎点名哪扇门关了(闭三词集 `mandated`/`shadowed`/`lane_cannot_speak`, * 引擎侧与 `ruleOffers` 互斥)。壳据它把「为什么没有不再询问」渲成对应文案(`mandated` 臂**不得** * 指向写规则)。ADVISORY 展示元数据、永不是裁决输入;**缺席不是断言**(covers「有 offers」与三扇 * 结构门)。值经 `approval-card.ts` 的 `readRuleOffersAbsence` 窄读(闭集校验;与 `card_json` * **同一个**函数 ⇒ 两面同值),词表外的值=形不合=不铸键([ref] 闭集纪律,绝不透传未知词)。 */ ruleOffersAbsence?: RuleOffersAbsence; /** * S-114(core 7.4.0 [ref];**ADDITIVE**,`"tool_approval"` only)—— 这只 ask 是 auto 模式分类器的**限额 * 回落卡**:分类器连续(缺省 3)或累计(缺省 20)拒到限额的**那一次**调用不再静默 deny,而是落成一张 * 必须真人批的卡(同一次铸造同时盖 `requiresRealApproval`,两键是孪生)。 * `{consecutive, total, limit:"consecutive"|"total", autoDenyAfterMs}` —— 触限时的两个计数、哪一道界 * 触了、以及**这张卡自己**的自动拒窗(ms;`0` = 不武装)。 * * 🔴 **additive echo-only,server 零消费**(`ruleOffers` 先例逐字):窗的执行全在 core 的 `resolveAsk` * ([ref] 的引擎自有 deadline —— 窗到即 deny 且盖 `settledBy:"timeout"` / `resolution:"window_expired"` / * `autoDenied:true`)。本仓**不得**据 `autoDenyAfterMs` 自铸第二只定时器:那会与上游的窗构成同一语义面 * 的两个写者,而两只窗的重叠比原缺陷更坏且静默(源头修复纪律)。 * * 值经 `approval-card.ts` 的 `readDenialLimitFallback` 窄读(`.strict()` 形校验;与 `card_json` * **同一个**函数 ⇒ 两面同值),形不合/闭集外 ⇒ 不铸整键。四成员皆机器数/闭词、非内容族 ⇒ 零 redact。 * **缺席不是断言**:绝大多数 ask 根本不是回落卡,消费端只读在场。 * * ⚠️ 耐久腿(park 行)今天**不携**本键 —— core 7.4.0 的 `PendingAction` 上没有这一位(亲读 * `dist/core/checkpoint-store.d.ts`,零命中);core [ref] 预告在 7.4.x,本仓**只登记不预铸**。 * 卡面那一份(`ApprovalCardSchema.denialLimitFallback`)走的是 `card_json`,与 park 行的 `pendingAction` * 是两条腿,别混。 */ denialLimitFallback?: DenialLimitFallback; /** * [ref] 件 G1(core 5.33.0 backlog [ref],判据帖 [ref] G1;**ADDITIVE**,`"tool_approval"` only)—— * 这次 ask **为什么**被收紧的**结构化**因由(`AskRequest.probeCause`:工具的 `reversibilityProbe` * 判不出可回滚时,引擎在 maybe 档收紧点铸并 `normalizeProbeCause` 校验过的那一只)。 * * 🔴 **结构就是契约**:`{ code, roots:{shown,total}, further?:{shown,total} }` —— `code` 机器可读、 * 两个操作数族各带真实基数。引擎的原话是「ships a code and operand arrays, never a finished * sentence」,句子归呈卡端渲。⇒ server **逐字透传结构**,不拍扁成文本、不重排、不合并两族。 * 值经 `approval-card.ts` 的 `readProbeCause` 窄读(与 `card_json` **同一个**函数 ⇒ 两面同值); * 形不合按缺席处置,`code` 超限整只丢、`shown` 超限截条目而 `total` 保真(理由见那里的顶注)。 * * 🔴 **与 `message` 刻意分列**(判据帖 G1 的负控):`message` 是分类器/模型的输入面,cause 永不进它; * 本键是展示面的结构化数据。负控钉在 `test/approval-probe-cause-wire.test.ts` §3。 * * 🔴 **缺席 ≠「没有原因」**:只有 maybe 档收紧且探针真给了 cause 的 ask 才有它。 * * 耐久路的对偶 = `gate.riskDescriptor.probeCause`(同值,core `buildRiskDescriptor` 铸),走 checkpoint * 行的**整体透传**到 `GET /v1/approvals` —— 键集零施工,钉在同一个文件的 §4。 * * 姊妹键 `AskRequest.probeReason`(同一件事的**散文**兄弟)**今天不投影**:它与本键是同一个事实的两种 * 拼法,而 core 的边界哲学(交 code + 数组、不交句子)与「一张卡上不放两份同义解释」都指向只投结构化 * 那一份;散文那份还无法被消费端本地化。照 `preview` / `requiresRealApproval` 先例 = 候消费端(cli) * 提渲染需求再翻(登记在 `trace/core-keyset-guard.ts` 的 EXCLUDED 栏)。 */ probeCause?: { code: string; roots: { shown: string[]; total: number; }; further?: { shown: string[]; total: number; }; }; /** * [ref] 件 G2(core 5.35.0 [ref] G-2;**ADDITIVE**,`"tool_approval"` only)—— 这只 ask 背后的 * **规则出处证据**(`AskRequest.ruleEvidence`,引擎在 gate 内盖章):org 快照 `orgRevision` / 命中的 * org 规则原文 `orgRule` / 被越级个人规则的 add dots `personalRuleDots`,**每员 = 值或五词命名缺席** * (`not_wired|not_adjudicated|unavailable|no_match|not_reported`,core `AskEvidenceAbsence` 闭集)。 * * 🔴 **缺席词表是本键的存在理由**:裸 `undefined` 对「没接治理层」与「治理源读不动」是同一个答案, * 审计链会把后者重构成「这次调用无治理」。`"unavailable"` 是承重员:org 层报了「看不见组织规则」, * 手上这只 ask 是随之而来的 fail-closed 收紧——不是任何已发布规则要求的 ask。 * * 🔴 **展示/对账元数据,永不是裁决输入**(core 契约逐字:removing it would leave every decision * byte-identical);server 逐字转录**引擎盖章**的那一份,**永不自铸缺席词**(server 铸词 = 谎报 * 「某一层没报」)。窄读走 `approval-card.ts` 的 `readRuleEvidence`(卡也用它 ⇒ 帧与 `card_json` * 同值;redact+截长在函数内一次算定,`persistedRuleShadowed` R2-[medium] 同款)。 * * 与 `persistedRuleShadowed` **刻意分列**:那是**人读的**越级规则原文(display 值,非身份通道), * 本键的 dots 是那条规则的**机器身份**(a rule is a set of adds;数组即身份,消费端禁 join 成标量)。 */ ruleEvidence?: RuleEvidenceProjection; /** "tool_approval" only: the tool call's args, secret-redacted, UNTRUSTED-for-display. Absent (with * `argsOmitted: true`) when over the byte cap or unserializable. */ args?: unknown; argsOmitted?: boolean; /** [ref] 随批小件([ref]-5 视觉真空的 server 半场;**ADDITIVE**,`"tool_approval_complete"` only, * 真才带)——`outcome:"expired"` 一词三义(park / 当场 deny / 无设施 deny,见 outcome 注)里 **park * 那一义的显式判别位**:在场 ⇔ **这条 ask 本身**按 park 路由收尾(`parkRouted`,settle 咽喉里置位; * [ref] F1/F3:首版读政策口〔unattendedPolicy+askStore 在场〕,对批内被连坐 VOID 的兄弟、取消/断连、 * D5 fail-open 等非 park 终局全撒谎)。⚠️ **判据是路由,不是墓碑**([ref] 起 `recordParkTombstone` * 在两种容量拒新形下不落墓碑,置位与落墓碑可不同步;取舍全文见 settle 咽喉那段注):常态墓碑同拍 * 已落 ⇒ 同一把 `approvalId` 仍可打 `respond` 走迟到受理兑现(wire 契约迟到受理段);拒新两形下 * live 迟到腿如实 404(unknown_or_other_replica),壳按既有协议回落 durable gate([ref] Q3)。 * 壳据此把「卡失效」改渲「已转后台候批」。缺席**禁**读作 * 「真 deny」:无店部署 / deny 政策 / VOID 兄弟都发不出这个键,缺席只是「无 park 证据」。 */ parked?: true; /** * "tool_approval_complete" only: how the ask settled. `allowed`/`denied` = a human decision. * * 🔴 `expired` = **这张卡失效了**(TTL / abort / 断连 / 批内兄弟被撤卡 superseded),**不等于「这次调用 * 被拒了」**([ref]:本注上一版逐字写着「fail-closed deny the shell should render as such」——那是 * [ref] 断连转 park 与 [ref] R-13 窗到期转 park **之前**的语义,两批都没回来改这一行)。同一个词今天有 * 三种落点,由部署决定,壳**不能**从这个词自己推出终局: * · park 设施在场 + 缺省 `park` 政策 ⇒ core durable park(run 挂起候补批,`shapeOutcome` 交 * `"unavailable"` 而本帧仍发 `expired`); * · `UNATTENDED_APPROVAL_POLICY=deny` ⇒ 当场 fail-closed deny(旧注只描述了这一支); * · 无 park 设施的部署 ⇒ core 自己 fail-closed deny。 * ⇒ 壳把它渲成「卡消失了」,**终局读 done 帧的 `status`/`errorCode`/`pendingGate`**。成文真源 = * docs/ASSISTANT-WIRE-CONTRACT.md §4a-bis(「别把 expired 本身当拒绝回执渲染」)。 * 成因表也补了第四支:`settleVoidedSiblings` 的撤卡兄弟走的同样是这个词,那既不是 TTL 也不是断连。 * Cosmetic dialog-dismiss(这句仍成立——它讲的是**帧的角色**,不是终局)。 */ outcome?: "allowed" | "denied" | "expired"; } /** The per-run context `ask` recovers via ALS (mirrors QuestionRunContext + sessionId, which keys the allow-all). */ /** [ref]:`GET /v1/approvals` 第二顶层键 `livePending` 的行形(与 durable `pending` 行**分数组不混编** * ——两族行形与决议路由不同:本族行的决议口=`POST /v1/tool-approvals/:approvalId/respond`,durable 行 * =decide 口;数组名即路由判据)。可选位全部「只记真、缺席不编」。 */ export interface LivePendingRow { /** = `respond()` 收的 wire id(pending Map 的 uuidv7 键;同源可达)。 */ approvalId: string; toolName: string; /** 登记时刻(ms epoch)。 */ ts: number; /** 窗绝对死线——与 `tool_approval` 帧 [ref] 三键同一次铸定的那个数。 */ expiresAtMs: number; sessionId?: string; requiresRealApproval?: true; governanceForced?: true; /** 发起者为委派子代(originTaskId 在场)。 */ fromSubagent?: true; /** [ref] C2(additive):委派子代的 **canonical taskId**。值域精确成文(codex R1-F3 问询后按 core dist * 亲证):值 = `AskRequest.sourceTaskId` = 发起 task 的 sessionId(core prepare-task `askSourceIdentity`); * 而 core 的 canonical taskId = `spec.taskId ?? sessionId`(runtask.js `runSourceTaskId`),Runner 铸的 * 委派子代 spec **不带** taskId ⇒ 子代 canonical taskId ≡ 子代 sessionId ≡ 本键 ≡ * `task_progress.taskId`(同一 `runSourceTaskId` 铸)—— 关联 join 在本键存在的唯一面(`fromSubagent` * 子代 ask)上恒等成立。缺席 = 顶层 ask。`fromSubagent` 保留不动(byte-compat:先于本键的消费方仍按 * 它判别)。 */ originTaskId?: string; /** [ref]([ref]②):内容问句分型 —— `"content_ask"` 当 `toolName` 为 AskUserQuestion(词属主 * `approval-content-kind.ts`,与 durable 两读面 / core summarize 面同词,OMIT 契约同族)。 * 缺席 = 普通工具 ask。展示/分诊分型用,永不参与决议路由。 */ contentKind?: "content_ask"; /** * 🔴 S-454(P1;cli L-397 / 板 [ref]):这只 ask **本来会 emit 的那只活卡帧**。 * * 值 = {@link PendingApproval.frame} 的**引用** —— `askBroadcast` 里那一处唯一构造(已过 * `redactDeep` / 字节帽 / [ref] 窗三键补写)的**同一个对象**,悬挂与可达两条路共用它。⇒ 这一格 * **不是**为列表另算的第二份卡投影:一处脱敏属主不变、零第二脱敏面,「wire 帧与列表行同一份判据」 * 是结构事实而不是两处各算一遍的巧合(同 `card_json` 与帧的关系)。 * * 🔴 为什么本行不再守「input/args 不上列表」那条窄化:那条纪律的理由原文是「详情走流帧」—— * 而**悬挂 ask 永远没有流帧**(铸造时刻零投递目标,`suspended`),对它这条理由结构上不成立。 * 消费方要出卡就必须在这一行上拿到卡体,否则「列得到但渲不出」。一视同仁给**全部** live 行 * (规则集不因 `suspended` 分叉:有活流的 ask 多带一份它自己那张卡的副本无害)。 * * 缺席 = 老 server(7.86.0 及以前);本服务自 7.87.0 起**恒在场**(条目上是必填格)。恒不铸 `null`。 */ frame?: ToolApprovalFrame; } export interface ToolApprovalRunContext { taskId: string; sessionId?: string; owner: string | null; emit: (frame: ToolApprovalFrame) => void | Promise; /** * [ref] 车6:**批级撤卡帧**(`approval_revoke`,语义与发射面见 approval-card.ts 的 * `ApprovalRevokeFrame` 顶注 —— [ref] 起 bg/resume durable 腿的钩子写本腿账本,sync 腿仍 live) * 的投递口。 * * 🔴 为什么是**独立的可选钩子**而不是给 `emit` 的入参加宽:`emit` 是既有 `tool_approval` 活卡腿的 * 通道,它的消费方(装配点 + 全部存量帧断言面)今天只认那两种帧;把入参改成联合会强迫每一个消费点 * 立刻处理一个它还不认识的帧形(存量测试的 `ToolApprovalFrame[]` 收集器首当其冲)。撤卡帧属于**新 * 协议**(`approval_request` 族)的一员,消费方是新壳 —— 分口投递是诚实的分层,装配点(车3 3b / * 车4 域)可以与 `approval_request` 的发射点**同批**接上。 * * 缺席 ⇒ 本连接不收撤卡帧;壳侧的结构补偿恒是重连 preamble 的全量对账基准。 * (接线现状:[ref] 起三腿全接 —— sync 腿 live 口(`routes/tasks.ts`),bg/resume 两腿 durable * append(`runs.ts` / `http/server.ts`),与 `emitCard` 的三腿同批对齐。) */ emitRevoke?: (frame: ApprovalRevokeFrame) => void | Promise; /** * [ref] 车3 刀 3b:**呈卡帧**(`approval_request`,[ref] §3.1)的投递口。 * * 🔴 与 {@link ToolApprovalRunContext.emitRevoke} 同一条理由(见其顶注):新协议族的帧走**独立的可选 * 钩子**,不把 `emit` 的入参加宽成联合 —— `emit` 是既有 `tool_approval` 活卡腿的通道,它的全部消费点 * (三条装配腿 + 每一个存量 `ToolApprovalFrame[]` 收集器)今天只认那两种帧,加宽会强迫它们立刻处理一个 * 还不认识的帧形。设计稿 §4.3(a)「新帧同走 `emitApproval`、不新增出口」讲的是**投递面**(同一条 SSE / * 同一条 durable tail),不是同一个 TS 字段:三条装配腿都把本钩子接到与 `emit` **同一个**写出口上, * 于是 wire 上确实是一条面、两种帧。 * * 缺席 ⇒ 本连接不收呈卡帧(旧壳照常靠 `tool_approval` 工作);`askId` 未确认落盘时**也不发** * (设计稿 §2.2(b) 硬条款一:无持久身份不发新帧)。发射序锚定在 `tool_approval` **之后**(§4.2)。 */ emitCard?: (frame: ApprovalRequestFrame) => void | Promise; abortSignal?: AbortSignal; /** [ref]([ref] §3.1 askId 派生的腿轴;车3 刀 3a 换轴:原 `leg?: number` → `legKey?: string`)。 * = `sha256(resume checkpoint token)` 的 hex,**首腿缺席折空串**(空串是首腿的真值,不是「未知」)。 * 与 `ToolApprovalRunContext.taskId`(= 派生里的 `runId` 轴)一起,使同一 `(sourceTaskId, toolCallId)` * 在不同 run / 不同 resume 腿下铸出不同持久行(不会被上一腿或上一 run 已终结的行绊住)。 * 换轴动机与残留边界见 `approval-ask-machine.ts` 的 `deriveAskId` 头注。 * 真值由装配点(刀 3b)供给,本刀只立形 + 折空串兜底。 */ legKey?: string; /** [ref] 车2([ref] §3.3 窗长三元 D3):本 leg 的 walltime deadline(`performance.now()` 单调基, * NOT `Date.now()`)。缺席 ⇒ 有效窗退化成构造时的 `ttlMs`(现行为逐字不变)。在场且 * `legRemainingMs − windowMarginMs ≤ 0` ⇒ 不开窗,直接走窗到期同路——§3.3 不变量:窗不得把一个 * 可 park 的 ask 拖成 abort-deny。真值由车3 的装配点供给,本车只立形+消费。 */ legDeadlineMonotonic?: number; } export type ToolApprovalDecision = "allow" | "allow_session" | "deny"; /** * [ref] 车二:回决回执上 `ruleRefusal` 的**闭词表**(「不再询问」这次为什么没存上)。 * * 🔴 为什么是闭集而不是 `string`([ref] 词表纪律):这一格是 wire 可见的**机器可读**位 —— 壳要据它分 * 「这次不能存」(`rule_input_edited`:编辑过输入,换下一次)与「这台部署压根不供规则」 * (`rule_lane_unavailable`)。自由串会让每个消费端各自猜词,而 server 加一个新拒绝理由时没人会红。 * 三格 server 自铸 + 车道自己的四格(`CardRulePersisted["reason"]`,从那个类型**派生**而不是抄): * 车道加员 ⇒ 这里编译期自动跟。 */ export type RuleRefusalReason = Extract["reason"] | "rule_lane_unavailable" | "rule_input_edited" | "rule_not_offered" | "rule_store_error" /** [ref] 件7:这只 ask 的门来自运维治理层(`governanceForced`)⇒ 它不进规则车道。**与 * `rule_lane_unavailable` 刻意分词**:那个说的是「这台部署压根不供规则」(壳可以从此不渲这一格), * 这个说的是「规则车道好好的,只是**这一只** ask 归 operator 管」—— 折成同一个词会让壳把一台正常 * 部署整条车道判死。 */ | "rule_governance_forced" /** * [ref]:**这一行**没有可铸规则的素材。原为 durable(PARKED)迟到决议腿专属;自 live 腿也接上素材闸起**两腿同词** * (第四条成因:命令含脱敏器认得的凭据形 ⇒ 素材整只扣下,两腿同谓词)。 * * 成因闭集(三条,处置相同):①这只 ask 从没进过规则车道(治理档 / 无属主 / 引擎没铸候选 / args 里 * 读不出命令原字节)⇒ `rule_command` 列 NULL;②行由**加这两列之前**的构建落下;③行上的卡读不回来 * (信封 schema 判假)⇒ 候选表与工具名都取不到。 * * 🔴 **与 `rule_lane_unavailable` 刻意分词**(理由与上面那条 `rule_governance_forced` 逐字同源): * 那个词的语义是「这台部署压根不供规则」,壳据它可以**从此不渲这一格**;而这里的真相是「车道好好的, * 只是**这一行**上没有素材」—— 折成同一个词,一次 park 迟到答就能让壳把一台正常部署的整条车道判死。 * 静默忽略更不行(安全/审批轴禁静默 fail-open):人勾了「不再询问」而什么都没发生,他不会再勾第二次。 */ | "rule_material_absent"; /** * [ref]([ref])—— `persistRule` 的**两个显式臂**在 wire 上的解析产物。 * * `edited` 是判别位而不是「一个可选的第二字段」:候选臂的 `rule` 是**卡上那条候选的逐字文本**(定位键), * 编辑臂的 `rule` 是**人自己写的规则**。同一个字段两种含义,判别位必须显式带在同一个对象上。 */ export type ParsedPersistRule = { readonly kind: "text"; readonly rule: string; readonly edited: boolean; } /** [ref](cli 1.0.92 合窗):人勾了 **batch** offer——合取批没有单条文本可抄,选择键=帧上 * `ruleOffers` 的下标(卡=行素材=呈卡帧同源;server 侧兑付前另过防漂等式,见 * `persistRuleAfterDecision` 的 batch 臂)。窄开只指 batch:single 臂的防伪锚是文本等式, * index 不许旁路它。 */ | { readonly kind: "batch"; readonly batchOfferIndex: number; }; /** [ref]:`persistRule.rule` 的形/上限拒句(wire 可见文案的**唯一**成形口 —— 冻结在 * `test/api-error-text-freeze.test.ts` 的常量锚格,与 `src/http/` 桶里的 `sendError` 站点同纪律)。 */ export declare const PERSIST_RULE_TEXT_ERROR = "persistRule.rule must be a non-empty string of at most 512 characters"; /** [ref]:`persistRule.edited` 的形拒句。非 boolean **绝不静默当 false** —— 那会把一次「我要落自由文本」 * 悄悄折回候选臂,回来一句 `rule_not_offered`,而人以为自己写的规则被拒了。 */ export declare const PERSIST_RULE_EDITED_FLAG_ERROR = "persistRule.edited must be a boolean when present"; /** [ref]:batch 选择键的形拒句(负数/非整数/非数一律响亮 400——一个被 |0 折过的下标指向的是另一条 offer)。 */ export declare const PERSIST_RULE_BATCH_INDEX_ERROR = "persistRule.batchOfferIndex must be a non-negative integer when present"; /** [ref]:三臂互斥拒句。同场不静默取一——两臂说的是两次不同的授权,猜哪个都等于替人改主意。 */ export declare const PERSIST_RULE_BATCH_EXCLUSIVE_ERROR = "persistRule.batchOfferIndex is mutually exclusive with persistRule.rule / persistRule.edited \u2014 send exactly one arm"; /** * [ref] 定界5:回决体**不收 scope**。 * * 规则落在哪个 scope 由 server 从**授权发生地**自铸(`ruleScopeRootFor` 在铸卡时咨询一次)。让携带方 * 指定 scope = 让一次「在项目 A 里点的同意」写成一条覆盖项目 B(或全局)的常驻放行 —— 那是这条轴上 * 最贵的一次扩权。静默忽略同样不行:客户端会以为自己设上了。响亮拒,一句话说清谁拥有这一格。 */ export declare const PERSIST_RULE_SCOPE_REFUSED = "this endpoint does not accept a rule scope \u2014 the server mints it from where the approval happened (send neither `scope` nor `persistRule.scope`)"; /** Validate the respond body — the closed three-choice enum (rationale in the module header). */ export declare function parseToolApprovalResponse(body: unknown): { ok: true; value: ToolApprovalDecision; updatedInput?: unknown; persistRule?: ParsedPersistRule; note?: string; } | { ok: false; error: string; }; /** * [ref] R-13 C —— `UNATTENDED_APPROVAL_POLICY` 的**闭集**词表([ref]Q1「裁2:A+C」/[ref] 裁②)。 * * 语义轴 = 「一只治理 ask 到了、而**没有任何人**能答」时这条部署要什么终局: * · `park`(缺省)—— 交 `"unavailable"`,由 core 走 durable park:run 挂起候人(done(suspended) + * pendingGate),人回来补批就能续。要求部署真有 park 设施(`durableApproval` + checkpoint 店); * 没有的话 core 自己 fail-closed deny(文案自带「no durable approval gate is armed」)。 * · `deny` —— 真无人值守 / headless 部署的**显式**自声明:不积压 park,当场 deny 让模型自己改道。 * * 闭集 + 穷举 `switch`/三元的理由([ref] 无静默 fail-open):这条轴决定的是**权限门的终局方向**, * 一个拼错的词若被当成「未设」静默回缺省,运维会以为自己关掉了 park 积压而实际没有。坏值 boot 拒启 * (`config.ts` 的解析腿,[ref] A 档)。 */ export type UnattendedApprovalPolicy = "park" | "deny"; /** * [ref] 车3 刀 3b —— 「流内审批协议到底上不上场」的**单一谓词**(设计稿 §2.3 注入 / §2.4 能力面 / * §8.4 park 设施自检,三处同一份判据)。 * * 🔴 为什么必须是一个函数而不是三处各写一遍的合取式:车4 落地时曾在回决端点里放了一份局部的 * `backend.kind !== "local"` 临时判据,并在注释里写死「等能力面谓词落地就换掉本段 —— 两份判据长期并存 * 必然漂」。这就是那一车。三个消费点(协调器注入 / `/v1/capabilities` / 回决端点 501)现在读同一个符号, * 于是「能力面说 true」⟺「协调器真拿到了 askStore」⟺「回决端点真有账可 CAS」是**结构成立**的, * 不再靠三处注释互相提醒。 * * 四个合取项,缺一即不上场(每一项的缺席都有它自己的 `reason`,供启动期 info 与诊断分辨): * 1. `toolApprovalEnabled` —— 连活卡腿都没有,谈不上流内协议; * 2. `streamApprovalEnabled` —— 协议总开关(`STREAM_APPROVAL_ENABLED`,树上已**默认 ON**;显式 `false` * 是唯一干净还原键。默认极性与版本坐标的属主口径见 `config.ts` 的 `streamApprovalConfig()` 头注); * 3. `backend` 在场 —— env-only worker 没有 `StoreBackend` 本体; * 4. **park 设施在场**(§8.4)—— 缺席时「窗到期 ⇒ unavailable」在 core 侧没有降级目的地,结局是 * fail-closed deny,**比现状(5min 活卡、人能批)更差**。所以自检不满足 ⇒ **协议不上场**、现行 * `tool_approval` 活卡腿逐字保留,而不是「把 ask 推向一个不存在的目的地」(§14 属主照准,core [ref] * 回帖确认即原意)。 * * 🔴 **S-384:第 5 项(旧第 4 项 `volatile_ask_ledger` = 「账必须是持久的」,判据 `kind !== "local"`) * 已整条删除,词也从闭集里删了(零别名)**。它当年成立的唯一输入是 local 车道交的 `InMemoryApprovalAskStore`; * `FileApprovalAskStore` 落地后 `StoreBackend.approvalAsk()` 的三条腿全是持久店 ⇒ 这条合取项**没有任何 * 能让它为假的输入**,留着就是一条永不触发的规则(「修完让规则集更小」)。持久性的义务搬到 * `StoreBackend.approvalAsk()` 的接口契约上(那条注 + `wiring-governance-operator.test.ts` 的三腿机器钉)—— * 它是**装配面**的事实,而本谓词能看见的只有 `kind`,拿 `kind` 当持久性的代理正是这次要拆掉的那层间接。 */ export type StreamApprovalGate = { active: true; askStore: ApprovalAskStore; } | { active: false; reason: "no_tool_approval" | "protocol_disabled" | "no_backend" | "no_park_facility"; }; /** {@link resolveStreamApprovalGate} 的入参。`backend` 用**结构形**(不 import `StoreBackend`):本模块是 * 协调器的家,不该为一个布尔判据把整棵 store 依赖树拖进类型面。 */ export interface StreamApprovalGateInput { toolApprovalEnabled: boolean; /** 协议总开关。⚠️ 调用点一律写 `config.streamApproval?.enabled === true` —— **可选链的缺席臂折算 * false**(段整段缺席 ⇒ 协议不上场)。 * 🔴 当年这条写的理由是「缺席折算值 == 产品默认(那时是 OFF)」,所以 `?.` 纯粹无副作用;产品默认 * 现已翻 ON(`config.ts` 的 `streamApprovalConfig()`),两者**不再相等**,那条理由作废。现行的 * (更弱也更诚实的)判据:真实 boot 恒有此段(`config.ts` 无条件铸 + 白名单门盯着),`?.` 服务的只是 * 那些只填被测路由用得到的键的 stub-harness,而一个没填这个段的 harness 要的必然是「别上场」。 * **不要**照「缺席 = 与产品默认同」去推理。 */ streamApprovalEnabled: boolean; backend: { readonly kind: "mysql" | "pg" | "local"; approvalAsk(): ApprovalAskStore; } | undefined; /** park 设施在场性。本仓真码里 `checkpointStore` 的构造条件本身就是 * `backend?.checkpoint !== undefined && config.durableApproval`(main.ts),所以设计 §8.4 那条析取式的 * 「safety 词表非空」一支在**装配时刻**结构上不可达(core 的 irreversible/egress 集合来自 **per-task** * 的 `spec.tools[].irreversibility/egress`,composition root 看不到)——两条缺失形因此在本仓收敛成 * 同一个 `no_park_facility`(D-5/D-6 两钉打的是两种**输入**形,不是两个 reason)。 */ parkFacility: boolean; } export declare function resolveStreamApprovalGate(input: StreamApprovalGateInput): StreamApprovalGate; /** * [ref] 车3 刀 3b —— **一条执行腿的审批装配裁定**(sync / bg / resume **三腿共用**)。 * * 🔴 为什么必须是一个函数(codex 交叉复审 F3/F4,2026-08-06 真 finding):第一版把这套判断**只**写在 * `routes/tasks.ts` 的 sync 腿上,bg(`runs.ts`)与 resume(`server.ts`)两腿只把「新协议的口与轴」挂在 * `streamApprovalOn` 后面,**却无条件包了 ALS**。后果有两条,都是真的: * ① **开关关时行为变了** —— 这两条腿此前根本没有 approval ctx(`deps.onAsk` 拿不到 ctx ⇒ 恒 * `"unavailable"` ⇒ 恒 park),包上之后每只 ask 都会变成一张活卡并等满窗。A-1「开关关=逐字零变化」 * 当场破。 * ② **窗=0 在这两条腿上不生效** —— 运维显式关窗 / 贴 deadline 时,它们照样落行、注册资源、发帧, * 然后等一个 0ms 的定时器,而不是设计要求的「直接走 park,连卡都不发」。 * 一份判据、三处消费,这两类漂就结构性地不可能再发生。 * * 三源(§8.3)里本仓可执行的是后两源;第一源 `forceDurableGate` 在 HTTP 装配时刻不可解析(§14 §12-5 * 亲裁:不可读则该源不做,core 自身的 `runtimeCaps?.forceDurableGate` 消费臂结构性兜底——7.0.1 * `prepare-task.js` 的 durableMandate/contentMandate 与 durableQuestionFace 等), * 故不在本函数内。 * * 🔴 **[ref](半接线旋钮族)**:`windowZero` 的第一源(`STREAM_ASK_WINDOW_MS=0`)自此**不再挂在 * `streamApprovalOn` 后面** —— 那是部署级旋钮,它的语义不该因为这台机器有没有 durable ask 账而改变 * (`ApprovalLegAssembly.windowZero` 顶注写了那条病)。第三源(贴 deadline)照旧只在协议上场时参与。 * * **`windowZero` 的两件事必须同时做**(调用方契约):不包 ALS **且**注入 immediate-unavailable 闭包。 * 只做一件另一条路仍会呈卡。⚠️ [ref] 起**三腿都有** per-task `spec.onAsk` 装配点(sync=routes/tasks.ts, * bg=runs.ts,resume=server.ts driveResumeLeg),windowZero 的第二件在三腿都是显式闭包 —— 旧文「bg/ * resume 无装配点,不包 ALS 即等价」的缺席论证已退役,别再据它推断「少装一件也行」。 */ export interface ApprovalLegAssembly { /** 协议在本腿上是否上场。false ⇒ **既不包 ALS、也不接** `emitCard`/`emitRevoke`/`legKey`/ * `legDeadlineMonotonic` —— **协议件**逐字保持协议之前的行为。 * ⚠️ [ref] 门职分离(codex R1-F1 随修):本位**不再兼职**「审批席在场性」—— `spec.onAsk = boundAsk` * 三腿恒装(判据 = 协调器在场,即 `TOOL_APPROVAL_ENABLED`),协议关时 bg/resume 腿的 ask 也能命中 * 同 (owner, session) 活流呈活卡(sync 腿 [ref] 起本就如此;bg/resume 的这条行为差 = [ref] CHANGELOG * 披露的行为面变更)。旧文「本腿逐字保持协议之前的行为」只对协议件成立,别再把它读成「协议关 = 零呈卡」。 */ active: boolean; /** §8.3 窗=0 恒 park。 * 🔴 **[ref] 起它与 `active` 解耦**(旧文「`active` 为假时恒 false —— 协议都没上场,谈不上关窗」已 * 作废):`STREAM_ASK_WINDOW_MS` 是**部署级**旋钮,`0` 的语义(运维显式关窗 ⇒ 恒走 * {@link ToolApprovalCoordinator.unattendedAskOutcome},连卡都不发)在两条车道上必须是同一个意思。 * 旧形下 local 车道(S-384 之前的「账不持久」臂)/ 无 park 设施 / 显式关协议的部署配了 `0`,拿到的是一张 * 等满 5min 的活卡 —— 与它自己的域注正好相反,且没有任何一行说出来。 * ⚠️ 「窗**缺席**」(`windowMs: undefined` = 这份装配根本没有 streamApproval 段,只出现在 stub-harness) * **不是**关窗:那时逐字保持活卡腿。 */ windowZero: boolean; /** §7.3 本 leg 的 walltime deadline(`performance.now()` 单调基)。只在 `active ∧ ¬windowZero ∧ * 有 walltime 墙` 时在场 —— 关着时供值会把既有 5min 活卡窗按三元公式缩短(§7.2 真行为变更)。 */ legDeadlineMonotonic?: number; } /** * [ref] 车3 刀 3b(codex 交叉复审 round2 R2-1,2026-08-06 真 finding)—— 呈卡帧的**双写投递口**。 * * 🔴 为什么必须收成一个有属主的工厂:round1 之后「新帧送达」开始**参与**「卡到底有没有送到人手上」的 * 判定(见 `emitOne` 顶注)。而 sync 腿原来那个内联闭包把两个 sink 的失败都吞掉、然后**正常返回** —— * 于是「账本写失败 ∧ socket 已死」这种**真的一路都没送到**的情形被上报成成功,`anySucceeded` 因此压住了 * park 路由,ask 会一直挂到窗到期而**没有任何人可能回答它**。诚实的形只有一个:**至少一个 sink 真的接下 * 了才算送达**,一个都没接下就抛 —— 调用点(`emitOne`)的 catch 会把它如实记成 `card: false`。 * * 顺序 = **先账本后 live**(与 file_link 的先例相反,理由是本帧的用武之地恰好在 live 面已死的时候): * detach 断连后 live 写被丢弃,而壳换道 events tail 必须还能看到这张未决卡(§4.3(c) 的案A)。 * * 命名(CLAUDE.md 工厂命名律):`create*` —— 返回的是**捕获了两个 sink 的闭包**,不是纯数据。 */ export declare function createApprovalCardEmitter(sinks: { /** durable 账本口(bg/resume 腿恒有;sync 腿只有 detach 车道有)。抛 = 这一路没接下。 */ appendDurable?: (frame: ApprovalRequestFrame) => Promise; /** live SSE 口。**已死的流应当由调用方判成缺席或让它抛**,不要写一个静默 no-op —— 那正是本 finding。 */ writeLive?: (frame: ApprovalRequestFrame) => void | Promise; }): (frame: ApprovalRequestFrame) => Promise; export declare function resolveApprovalLeg(input: { /** = `resolveStreamApprovalGate(...).active`(协议在本 worker 上场没有)。 */ streamApprovalOn: boolean; /** `config.streamApproval.windowMs`(`STREAM_ASK_WINDOW_MS`)。 * 🔴 [ref]:`undefined` = **这份装配没有 `streamApproval` 段**(只出现在只填被测键的 stub-harness —— * 真实 boot 无条件铸该段,见 `config.ts`)。此前三个调用点各写 `?? 0` 把这一形折成「0」,在旧语义下 * 是死代码(`windowZero` 那时挂在 `streamApprovalOn` 后面,协议关就恒 false);[ref] 让 `0` 在协议关的 * 腿上也真生效之后,那个折算会把一台**什么都没配**的 harness 判成「运维显式关窗」。故本字段收成 * `number | undefined`,由每个调用点如实交出「有没有这个值」,不许再就地编一个。 */ windowMs: number | undefined; /** `config.streamAskWindowMarginMs`(`STREAM_ASK_WINDOW_MARGIN_MS`)。 */ windowMarginMs: number; /** `spec.limits.maxWalltimeMs`。缺席 = 没有 walltime 墙 ⇒ 第三源不参与(**缺席 ≠ 0**)。 */ legWalltimeMs?: number; /** 本腿**开始执行的时刻**的 `performance.now()`。取值点绝不能挪到腿启动之后:误差方向必须是 * 「窗偏短」(安全侧,§7.3 的误差方向论证)。 */ nowMonotonicMs: number; }): ApprovalLegAssembly; /** * Coordinates the live tool-approval HITL for the singleton runner. Process-local + same-replica (the pending map is * in memory, like QuestionCoordinator): a respond that lands on another replica finds nothing → 404. Present (passed * into `RunnerDeps.onAsk` + the respond route) ONLY when `TOOL_APPROVAL_ENABLED` — absent ⇒ core's `resolveAsk` * keeps its headless auto-deny for every ask ([ref]⑤ fail-closed default, zero behavior change). * * G1 interplay (core 1.290 sync-ask knob, prepare-task): with an onAsk present, a NON-safety ask with NO per-task * `durableApproval` skips the durable park and resolves on this sync leg (zero checkpoint, CC-interactive latency). * A durable-approval deployment (`DURABLE_APPROVAL=true` sets spec.durableApproval on every task) keeps parking every * ask durably — this bridge then only serves legs where the park machinery is absent. Safety asks (irreversible/ * egress) always keep the durable park when a checkpointStore exists (core's condition, not ours). */ export declare class ToolApprovalCoordinator { private readonly als; private readonly pending; /** [ref] codex R3-1 / R4-1:终局阶梯**在飞店调用**(claim + 让路探针读)的计数 —— 按**持久 askId** 记(不按 * 本地登记闭包:同 askId 的重复登记 `pendingByAskId` 各领一顶帽 = 帽可被登记数绕开,池位按行数才是帽的 * 本义)。原始店 promise settle(resolve/reject 皆是)归还,归零即删键;帽值 {@link MAX_INFLIGHT_TERMINAL_CLAIMS}。 */ private readonly terminalClaimsInFlight; /** {@link terminalClaimsInFlight} 的唯一写口:申请一格(帽满 ⇒ `undefined`),返回归还闭包。 */ private admitTerminalClaim; /** 某 askId 此刻的在飞终局店调用数(claim + 让路探针)。阶梯据它分「调度争用」与「无人在飞」(R6-1); * 判据钩子同读此口。 */ private terminalClaimsInFlightCount; /** 判据钩子(生产路径不调)。 */ terminalClaimsInFlightForTest(askId: string): number; /** [ref] 车2(codex 交叉复审 round3 抓获真 finding,round5 精化成 Set):次级索引,键 = 持久层 askId * (与 {@link pending} 的 wire-面 uuidv7 `id` 是两条独立的身份轴,顶注同精神)——只在 askStore 在场且 * 这只 ask 真有 askId 时才登记。**同一 askId 下可能同时挂着不止一条本地条目**(round5 抓获: * `ensureAsk` 是幂等 upsert,若 core 对同一 (sourceTaskId,runId,toolCallId,legKey) 真发起过两次并发调用——如 * 重试/failover 场景,round4 finding2 的顶注同源——两次 `askBroadcast` 各自的调用栈都会在行仍是 * STREAM_PENDING 时各自注册一条独立的本地 pending 条目,值形若是单值 Map 会被后到者覆盖前者的登记, * 前者从此再也没有任何本地事件会驱动它 settle),故值形是 `Set`,不是单个 `PendingApproval`。 * 存在理由(两类真实成因共用同一套修复):①同一批(相同 sourceTaskId+leg)下若有多只**不同** ask 并发 * 在飞(不同 toolCallId,同一 batchId),`expireAsk` 赢家的同一次持久事务会把批内其余 STREAM_PENDING * 兄弟原子撤成 VOID 并把它们的 askId 列在 `voidedSiblings` 里;②同一 askId 的**重复本地注册**(见上)。 * 两类情形下,那些"没赢"的本地条目各自独立的 timer/settle 闭包都对"这只 askId 其实已经有了终局"一无 * 所知:它们自己的窗到期/取消若稍后也去打 CAS,只会干净地输(D2 round2「输⇒什么都不做」),从此再没有 * 任何本地事件会驱动它们 settle——TTL 形同虚设,ask 会挂到进程重启。这个索引让赢家能反过来找到同一 * askId 下全部本地条目(自己 + 兄弟 + 重复注册),直接调用它们各自的 `settle`,而不是被动等一个永远 * 不会来的本地信号。 */ private readonly pendingByAskId; /** Per-capability session grants — keys = sessionAllowKey(owner, sessionId, category) (修2; bounded). */ private readonly allowAllSessions; /** [ref] HIGH-1(broker)→[ref]四 core 产品裁定「多活集合+广播+首决胜出」:per-(owner, host-session) * 的**当前活跃连接集合**——boundAsk 闭包只携身份,emit 时广播给该 key 下**全部**活连接(宿主重连/ * attach 新 SSE 时 runWithContext 各自注册进同一集合,谁也不覆盖谁);查无活连接 = "unavailable" * (G1 回路)。旧单指针形的缺口(file-backed local mode 无单活跃 run 409 守卫时,两个真正并发的 * runWithContext 会让后到者的指针覆盖先到者,先到连接的卡静默不可达——core 点名与「1.334 标已告知 * 须以投递确认为前提」同族)已随本集合改造解:后到者只是加入集合,不挤走先到者。首个 respond() * 经 `pending` map 天然 CAS 胜出(第二个 respond() 撞 id 已被删 → 404),其余连接靠 completion 帧 * 收 dismiss 通知(见 askBroadcast)。 */ private readonly streams; private readonly ttlMs; /** [ref](案A §3.4):**零活流铸造**的 ask 的窗(`UNREACHED_ASK_TTL_MS`,缺省 * {@link DEFAULT_UNREACHED_ASK_TTL_MS} = 1h,域 [60s, 24h] 由 config 守)。只作用于 * `AskReachability === "unreached"` 的那一支;有活流的 ask 恒用 {@link ttlMs}。 */ private readonly unreachedTtlMs; /** [ref] 车2([ref] §7 协调器半场,D1):可选持久层——缺席 ⇒ 每一条现行为逐字不变(in-memory * promise 机械即契约);在场 ⇒ 四竞争者(回决/窗到期/取消/emit 全灭)的终局多一道持久 CAS 记账。 * additive 改造,不是替换——settle 闭包/pending map/TTL/broker 全保留,CAS 只决定「谁有权 settle * 成什么终局」。 */ private readonly askStore?; /** [ref] 车2([ref] §3.3 D3):窗长三元公式的安全余量,构造期定,详见 {@link effectiveAskWindowMs}。 */ private readonly windowMarginMs; /** [ref] 车3 刀 3b(设计稿 §0 X-2 写侧准入门):per-task / per-owner 的未决 ask 上限。 */ private readonly admitMaxPerTask; private readonly admitMaxPerOwner; /** X-2 的两把**在飞计数**。键 = 出处 taskId / owner(`null` 折一个不可能与真 principal 相撞的哨兵)。 * 值在**准入那一刻**加、在 `settle`(或准入后的任何早退路径)减 —— 计的是「已经分配了资源的未决 ask」, * 不是「注册进 pending map 的条目」:两者之间隔着 `ensureAsk` 的一次 await,只在注册时计数会让并发的 * 一群 ask 全部越过门。 */ private readonly admitByTask; private readonly admitByOwner; /** [ref] F4:悬挂决议轮询腿的**轮转游标**(整数偏移,与集合成员无关 —— 理由见 * `pollSuspendedDecisions` 里那段:按 askId 记的形会在「上一拍最后碰的那条被结算掉」时塌回队首, * 构造出永久饥饿的前缀)。只回答「上一拍扫到第几个」,单调推进、按当前长度取模。 */ private suspendedPollOffset; /** [ref] + codex R3-[critical]:askId → **此刻正带着 `updatedInput` 决它的请求数**(见 * {@link markEditedDecisionInFlight} 顶注)。只在 `decideAsk` 那一次调用期间非空,故天然有界。 */ private readonly editedDecisionsInFlight; /** [ref] 车2(D5 一次性 warn 节流):同实例只报第一次,后续只计数(避免 store 抖动期间刷屏)。 */ private storeErrorWarned; private storeErrorTally; /** [ref]/[ref] `governanceForced` 的**读侧**表(写侧 = runtime-governance 的两只观察器)。 * 缺席(生产形)⇒ 每条 run 腿由 {@link runWithContext} 现铸一张、经 ALS 与写侧共享;在场 ⇒ 测试注入的 * 固定表(此时不进 ALS 作用域,读写都走这一张)。作用域理由见 `governance-ask-marks.ts` 顶注。 */ private readonly governanceAskMarks; /** [ref] 车二:持久化权限规则的**同意车道**。在场 ⇔ 规则店真装配(main.ts 与 core 的 * `RunnerDeps.permissionRuleStore` 同源于一个对象)⇒ ①ask 帧投 `ruleOffers`;②回决带 * `persistRule` 时兑付进店。缺席 ⇒ 两件都不做(诚实缺席,不发无处可兑的候选)。 */ private readonly ruleConsent; /** [ref] R-13 C(`UNATTENDED_APPROVAL_POLICY`,设计稿 §2):**无人可答**时这条部署要的终局。 * `park`(缺省)= 交 `"unavailable"` 走 core durable park;`deny` = 显式自声明「真无人值守、不积压 * park、要模型当场自走」⇒ 同样五臂改判 deny。**纯部署级**:不看任何客户端 posture / 表态 * (memory: operator-knob-must-be-unconditional —— [ref] shellGate 让部署级旋钮的生死由客户端表态 * 决定,同病两犯过一次)。安全轴:`deny` 是收紧方向(不放行任何东西),fail-closed 铁律不受损。 */ private readonly unattendedPolicy; /** [ref](F-1,[ref] 修向 (b')):卡批「不再询问」落盘规则的 **project root 解析器**。铸卡素材时以 * ask 的 `sessionId` 咨询一次——「这次授权是在**哪里**点的」在授权发生时捕获,不在回决时再猜。 * 回 `undefined` = 该部署形上 run 的 workspace root 不可知(远程沙箱/多租户等)⇒ 不铸 scope,落 * core 的 global 缺省(= 现行为,恒不更宽;也绝不错铸一个坐标系不对的 root)。装配见 main.ts: * registry cwd(host lane 显式注册的 launch dir)?? in-process 单用户形的 `process.cwd()`。 */ private readonly ruleScopeRootFor; /** [ref]:park 路由墓碑表(键 = wire `approvalId`,插入序 = 逐出序)。语义与有界理由见 * {@link ParkTombstone} 顶注。 */ private readonly parkTombstones; private readonly parkTombstoneMax; private readonly parkTombstoneTtlMs; /** [ref]:per-principal 子帽(语义与取值理由见 {@link DEFAULT_PARK_TOMBSTONE_PER_PRINCIPAL_MAX})。 */ private readonly parkTombstonePerPrincipalMax; /** [ref]:PARKED 臂的赎回席**取值口**(晚绑 —— 赎回腿要 checkpoint/bg 店与裸 Agent 工具,它们在协调器 * 构造之后才装配;同款 holder 先例 = main.ts 的 `getRunDenySweep`)。取到 undefined ⇒ 该臂逐字回落 * 修前 404,partial 部署安全。 */ private readonly parkedRedeem; /** [ref] 裁 (c):local 车道的 File ask **审计** sink(在场 ⇔ `DB_BACKEND=local` ∧ 审批面开着,装配点 * `boot/coordinators.ts`)。三个挂线点:铸造(askBroadcast register 后)/ 决议(`settle()` 公共咽喉)/ * 腿闭(`runWithContext` finally)。**纯观察面**:sink 自己吞写失败并 `recordFailOpen`,任何一次审计 * 写都不得改变审批终局(店头注成文)。SQL 车道恒缺席(那里有真 ask 店)。 */ private readonly askAudit; constructor(opts?: { ttlMs?: number; /** [ref](案A):零活流铸造的 ask 的窗(见 {@link unreachedTtlMs})。装配点在协议**上场**时才传 * (与 `ttlMs` 同条件、同理由:悬挂的三条发现通道全长在 ask 行上);缺席 ⇒ 1h 代码默认。 */ unreachedTtlMs?: number; askStore?: ApprovalAskStore; windowMarginMs?: number; admitMaxPerTask?: number; admitMaxPerOwner?: number; /** 缺省 = 进程级单表(写侧默认同一张)。注入口只为测试与将来的多实例形。 */ governanceAskMarks?: GovernanceAskMarks; /** [ref] 车二:同意车道(在场 = 规则店已装配)。 */ ruleConsent?: RuleConsentLane; /** [ref] R-13 C:无人值守政策(见 {@link ToolApprovalCoordinator.unattendedPolicy});缺省 `park`。 */ unattendedPolicy?: UnattendedApprovalPolicy; /** [ref]:卡批规则的 project root 解析器(见 {@link ToolApprovalCoordinator.ruleScopeRootFor})。 */ ruleScopeRootFor?: (sessionId: string | undefined) => string | undefined; /** [ref]:PARKED 臂赎回席的**晚绑取值口**(见 {@link ToolApprovalCoordinator.parkedRedeem})。 */ parkedRedeem?: () => ParkedAskRedeem | undefined; /** [ref]:墓碑表两道界(缺省见 {@link DEFAULT_PARK_TOMBSTONE_MAX}/{@link DEFAULT_PARK_TOMBSTONE_TTL_MS})。 * 不是部署旋钮(不接 env),可注入只为让「有界」这件事被判据观测得到。 */ parkTombstoneMax?: number; parkTombstoneTtlMs?: number; /** [ref]:per-principal 子帽(同上不接 env;生效值恒 `min(本值, parkTombstoneMax)`)。 */ parkTombstonePerPrincipalMax?: number; /** [ref] (c):local 车道审计 sink(见 {@link ToolApprovalCoordinator.askAudit})。 */ askAudit?: ApprovalAskAuditSink; }); /** * [ref] R-13 C —— 「**无人可答**」这一类终局的**唯一**成形口(park 路由 vs deny 政策)。 * * 五臂(设计稿 §1 的表)全部读它,`unattendedPolicy` 因此是一处施加、五处消费: * (a) 有店窗到期 `expireAsk` 赢 CAS / (b) 无店窗到期(D1)/ (c) 有店但 `expireAsk` 报错 * —— 三条走 `windowRouteUnavailable` 标志,在 {@link shapeOutcome}(askBroadcast 内)成形; * (d) `windowZero`(装配层短路)与无 ALS 上下文的 headless 腿 —— 走本方法/{@link ask}; * (e) 断连 `forceParkNow` / emit 全灭 / 写侧准入超限 —— 走本方法。 * * **持久回放的两个口也归它管**(codex R1-F1 验真后收口,红先钉在 * `test/unattended-approval-policy.test.ts`):claim 败方真相的结算([ref] `settleFromClaimLoss`,前身 * `convergeFromDurableState`)与 `ensureAsk` 幂等重入,只要回放出来的是 `"unavailable"`(行 PARKING/PARKED)就过这个口。理由:那三处与臂 (a) 是**同一件 * 事**(本 ask 的等待以「行进了投递面」收场,赢家可能是别的副本 / 上一次调用),不统一的话同一只 ask * 的终局会取决于「谁赢了那把 CAS」「重试没重试」—— 比残余更坏的不可解释性。人的**真决议**(回放出 * `true`/`false`)当然不过这个口:那不是「无人可答」。 * * 🔴 **不**归它管的一类(刻意,不是遗漏):`ensureAsk` **身份未定 / 坏行**两臂 —— 那里的 park 路由是对 * **身份不确定**的处置(与「有没有人」无关),且背后可能正躺着一条真行。于是 `deny` 部署上仍可能落 * 极少数 park(店抖动期),已知且有意的残余,成文在 config-types 的域注。 * * ⚠️ **`deny` 部署上持久行仍会经 PARKING 记账**(codex R1-F1 的另一半,验真后**不采纳**其「给 deny * 造一条原子 deny 终态转移」的建议):那是店语义变更(新转移 + 批内兄弟连坐语义 + 真双库套件),且与 * 已裁的设计稿(§2:(a) 在 deny 下只改交给 core 的终局)相左。**危害亲验为零**:收敛器判据①的 * `bindBatch` 只把 PARKING 行绑到**已经存在的 core checkpoint** 上,而 `deny` 部署里 core 从不 park * ⇒ 结构上绑不上,该行按判据②/④/⑤ 自行收敛成 DENIED/VOID,不会凭空生出一张 park 或幻影 gate。 * 残余 = 那类行的归因会记成 `routing_failure`(其实是政策 deny),归因精化登记为候件。 */ private unattendedOutcome; /** 政策**读**口(不是终局铸口)—— 回答「这台部署把无人可答翻成 park 吗」。 * * 🔴 与 {@link unattendedOutcome} 分家是 S-187 的一半:此前两件事共用一只方法,于是墓碑腿那次 * 纯粹的**政策查询**(`unattendedOutcome() !== false`)会和同一只 ask 的真终局各记一条遥测 —— * 「一只 ask 一条线」当场不成立。读口不落日志,铸口落;谁也不兼职。 */ private get unattendedRoutesToPark(); /** {@link unattendedOutcome} 的**公开**口:`windowZero` 的 immediate-unavailable 闭包长在装配层 * (`http/routes/tasks.ts` 的 sync 腿),它必须与协调器内五臂读**同一个**旋钮值,而不是各算一遍。 * 出处随调用方给(那条腿知道自己是哪一只 ask)。 */ unattendedAskOutcome(site: { taskId?: string; owner?: string | null; toolName?: string; }): "unavailable" | false; /** * [ref] 车3 刀 3b —— [ref] §3.3 / 设计稿 §0 X-2 的**写侧准入门**。 * * 位置(硬条款):在落 `pending`、建 timer、`ensureAsk` 落行、发帧**之前**。资源分配发生在**创建侧** * (每只未决 ask 各带一条持久行 + 一只 timer + 一个 promise + 一份 SSE 载荷;`pending` map 本身无容量 * 上限,`MAX_ALLOW_SESSIONS` 只管 grant 集合),所以回决腿与恢复扫描都只能在**已经分配之后**动手 —— * 门必须长在这里。 * * 超限的处置是 `"unavailable"`(park 路由),**缺省 park 政策下永不 deny**:过载是**我方**的容量事实,不是人对这次 * 操作的判断;用 deny 表达过载会把一次「本可以 park 后由人补批」的操作变成任务失败(§3.3 硬条款)。 * * 只在 `askStore` 在场(= 协议开着)时把关:开关关闭时本方法恒放行,现行为逐字不变(D1/A-1)。 * * ⚠️ **已认领的偏离**(设计 X-2 字面要求「计数在持久层做」):v1 是**进程内**计数 —— 它对单副本完整 * 有效,多副本下每个副本各自把关(真实上限 = N × 帽)。理由=持久层计数要么在每只 ask 的热路径上多一次 * 往返(与「门必须在分配之前」叠加成两跳),要么引入一张新的计数表与它自己的收敛问题;而本门的目的是 * **防单腿失控**(一个疯狂 ask 的 run 打爆本副本的内存/连接),那正是进程内计数能完整覆盖的形。 * 多副本级的总量控制登记为后续件(汇报存疑单)。 */ private admit; /** * [ref] 车二:一只 ask 的**规则车道素材**,或 `undefined`(= 本 ask 不投候选、不接受 `persistRule`)。 * * 三个合取项(缺一即 `undefined`,理由逐字见调用点上方注):①店在场 ②引擎铸了候选 ③命令原字节可读。 * `req.args` 是 `unknown` ⇒ 窄读,**禁裸 as-cast**(宪法 [ref]:一个形状漂了的 args 若被 cast, * 会把 `undefined` 当命令送进 `prepareCardApproval`,那是放宽面上的静默垃圾)。 */ /** 两份候选是否**逐条逐键**相等(顺序即展示序 ⇒ 顺序敏感)。两侧都缺席 = 相等;一侧缺席 = 不等。 * 用在幂等重入的「行 = 真源」对账上(见 `ensureAsk` 那段撤回臂)。 */ private static ruleOffersEqual; private static batchMemberEqual; private static uncoveredDetailEqual; /** * [ref] / 5.58 复审 F-1 —— OFFER 的**文本座尺**(红先修)。 * * 🔴 病灶(真复现,`npm run build --flag=<700 字> && git status`):`batch` 成员的 `segment` 是复合命令 * 那一段的**原字节**,它**不经** `parseRuleText`,所以 core 的 `MAX_RULE_TEXT_CHARS` 对它一个字 * 都不管;而卡的 `ApprovalCardSchema` 对四个文本座一律 `.max(MAX_RULE_TEXT_CHARS)`。逐字拷进卡 ⇒ * `card_json` 落库 ⇒ `ensureAsk` 当场回读 `safeParse` 判假 ⇒ `persisted-row-unusable` ⇒ **整只 ask 走 * park**,重放腿一并跳过该行。与「重扫二轮 §基数帽」是**同一条悬崖的另一条边**(那次是 offer 条数 * 没执法,这次是文本长度),5.58 换形之前不存在——旧 `RuleSuggestion` 三个座全是解析器输出,结构性 ≤512。 * * 两个座两种处置,**刻意不同**: * · `segment` = **纯渲染座**(core 明写「never adjudication input」)⇒ 截长是诚实省略,与 * `toolName`/`message` 的 `clip` 同待遇,也与耐久腿 `boundedRuleOffers` 的同名处置同形。 * · `rule`/`command` = **兑付等式的左边**(回决 `persistRule.rule` 按文本在引擎重铸的 offer 表里定位) * ⇒ 截一个字就恒 `rule_not_offered`,所以**绝不截**;真超尺(= core 放宽了自己的 512 或本仓与 core * 版本不同步)⇒ **整条车道撤回**(返回 `undefined`),与本文件既有的治理撤回 / 漂移撤回逐字同形: * 少渲一格是安全方向,渲一格按不动的才是 wire 谎言。今天这条臂不可达(每个座都是解析器产物), * 它守的是版本不同步那一形 —— 与 `isSupportedRuleMatch` 的运行期 `default` 同族。 * * 位置在**素材铸点**(与基数帽同一处):两族帧 + `card_json` + 回决口的等式左边从此拿的是**同一份** * 已截素材,而不是两处各截一遍。 */ /** 兑付侧座(single 的 rule/command、command 成员的 rule/command、directoryRead 成员的 rule/directory)任一超尺? * `segment` 与 `uncoveredDetail[].segment` 是纯渲染座,不在此列(截长是诚实省略)。 */ private static ruleOfferTextsOverSize; /** codex r1 [high]([ref]):offer 的自称座与 `parseRuleText(rule, "allow")` 派生的三元组逐字一致?(理由见调用点) */ private static ruleOfferDerivedFactsAgree; private static boundRuleOfferTexts; private buildRuleLaneMaterial; /** [ref]([ref]/[ref] 双属主裁定):把某 wire run 的**流内未决 ask** 立即转 durable park——断连支专用。 * 匹配键=`ctxTaskId`(wire run id)。该字段的顶注说它「不是清扫判据」——那是因为清扫要判**连接**的 * 生死(同 id 可被两个 ctx 实例复用);本臂语义不同:调用方就是该 wire run 的承载流,断连=这个 id 的 * 流没了,值相等即目标集;极端复用形下多转的那条也只是从流内卡变 durable 卡,方向保守。 * 返回触发条数(0 = 无流内未决 ask,调用方照旧 abort)。幂等:已结算条目跳过,重复调用无害。 */ parkOpenAsksForTask(wireTaskId: string): number; /** 测试/可观测性钩子(X-2):某 (taskId) 维当前占用的准入名额数。 */ admittedCount(taskId: string): number; /** [ref] 车2(D5 store 故障姿势):任何 store 调用 throw ⇒ fail-open 到进程内机械照旧(store 是记账/ * 收敛层,不是投递面——裁决可用性不因它抖动而降级),一次性 `logger.warn`(同实例只报第一次,后续 * 只计数)。**区分**「store threw」(本方法专管)与「CAS 输」(如实拒绝,D2 必须服从,绝不算故障、 * 绝不走本方法)。 */ private noteStoreError; /** [ref] 车5(R2-5):给一次 store 调用套墙钟上限。超时 = 以 `Error` 拒绝 ⇒ 调用点既有的 catch(D5 * fail-open / 防御性复核)原样接住,不需要为超时新增一条语义。定时器一律 `unref`(绝不持住进程), * 竞速输的那一路由 `Promise.race` 自己的 rejection handler 接住(不会变成 unhandled rejection)。 */ private withStoreDeadline; /** 测试/可观测性钩子(D5):store 调用失败的累计次数(第一次触发 warn,其余只计数——本方法让「只计数」 * 那部分可断言)。 */ storeErrorCount(): number; /** [ref] 车2(codex 交叉复审 round3 抓获、round5 精化,真 finding,{@link pendingByAskId} 顶注有完整 * 背景):某个 ask 赢下持久 CAS 时,同一 askId 下**全部**其余本地条目(批内被原子撤卡的兄弟、或同一 * askId 的重复本地注册——两类成因,{@link pendingByAskId} 顶注)都对"这只 askId 其实已经有了终局"一无 * 所知,任其自生自灭会让它们的 TTL 形同虚设(round2 之后,它们自己的窗到期/取消一旦发现 CAS 已经干净 * 地输,只会「什么都不做」,永远等不到本地事件驱动 settle)。这里直接反查这些 askId 各自的本地 pending * 条目集合并逐个调用它们自己的 `settle` 闭包(复用它们自己的 timer 清理/abort 监听器摘除/pending * 删除),把远程/其他调用栈发生的终局如实同步回本地——查无对应条目(不在这个副本、或从未真正注册过) * 是正常情况,静默跳过。 * * 🔴 **本口只收「行真的没了」那一类**([ref] codex R3-F2,验真后拆分;红先钉在 * `test/unattended-approval-policy.test.ts`):`expireAsk` 返回的 `voidedSiblings` 的行是 **VOID**, * 裸 `(false,"expired")` 正是它们的真终局。而**调用方自己那个 askId** 的行是 **PARKING**(已转投递 * 面)—— 把它混进本名单会让「同 askId 的重复本地注册」拿到裸 deny,而赢家拿的是 park 路由:同一条 * 持久行、两种 live 终局。那一支改走 {@link settleSameAskIdParkRoute}。 */ private settleVoidedSiblings; /** * 🔴 session grant 短路前的**持久出处复核**([ref] 的 codex R1-[high] 补丁;[ref] 换判据)。 * * 回 `true` = 「这只 ask 从来没有过持久出处」⇒ 一揽子放行可以短路; * 回 `false` = 行在(任何状态),**或**读不出来(不知道 = 不放宽)⇒ 调用方必须往下走真出卡。 * * 为什么只读不写:短路的全部价值就是「不铸卡、不落行、不占 pending」,做一次主键点读保住这个价值, * 而 `ensureAsk` 会写行。为什么行不在就放行:确定性 askId 的行只可能由**这只 ask 自己**铸出来, * 行不在 ⇒ 从来没有过一份能反驳本地判据的持久出处(这也是常见形:grant 命中的 ask 多半是头一次到)。 * * ## 判据的单一原则:**短路不得产生与「没有 grant 时那条路」不同的终局** * * 没有 grant 时,同一条行在下面 `askBroadcast` 里都有确定去处:终局行(DECIDED/PARKED/DENIED/VOID) * 走 `replayTerminalAskRow`(deny 回放 `false`、park 回放 `"unavailable"`);活着的 STREAM_PENDING 行走 * `ensureAsk` 的幂等命中(采信行上的卡与 deadline,等真决议)。短路一旦 return,这两条腿结构上都够不着 * —— 所以只要行在,短路就是在用一个**更宽**的答案顶替那条路。⇒ 判据 = `row === null`。 * * 这条原则是分两次到位的,两次漏的都是「行 = 真源」的一部分: * · **[ref]**(codex R1-[high]):首版只查**本地** peek,行上标治理的那一半漏了 —— 本地标表是 * 进程内表,failover / 重启后幂等命中既有行时它恒空。 * · **[ref]**(2026-08-19 三轴组复审,refuter 真运行复现三形):补丁把行读到手里,却只看 * `card.governanceForced` 一个位 —— 行处于 `DECIDED=deny`(人已明确拒过这只调用)时照样回 `true`, * 一揽子放行把一次**已落盘的人类拒绝**在重入 / failover / deny-后到 三形里翻成放行(帧 0、行不变、 * 无 recordFailOpen ⇒ 遥测零痕迹)。 * · **codex 交叉复审**([ref] 同批,验真后采纳):只补终态判据仍漏「卡还在别处挂着」—— * STREAM_PENDING 行意味着已有一张卡在等人,凭 grant 抢跑会让行上的终局与真实执行长期矛盾 * (那张卡随后可被人/另一副本决成相反答案,或 TTL 到点被收敛成幻影 gate)。 * 三次都不是各自独立的 bug,是同一句话没说全,故最终判据收成一条而不是三个分支。 * * 零额外 IO(行已经在手里),且 fail-closed **by construction**:将来 `AskState` 加词也不需要动这里。 * * 🔴 **已登记的残余([ref],PARTIAL,本批不修)**:这一步是一次**非原子的 check-then-act** —— * 另一副本正在为同一确定性 askId 落行、而本次点读抢在其提交前完成时,本副本仍会短路。真解 = 把出处 * 判定与 grant 消费合并成**同一次**原子存储操作(带条件 upsert / CAS),属店语义级改动,须本模块属主 * 立件(与上面 R4-F1/[ref] 两条同源残余同一处置)。「行不在 ⇒ 不短路」不是修法:grant 命中的 ask * 多半头一次到,那等于把一揽子放行整只废掉。窗的上界由本判据定死:行**一提交**窗即关。 */ private sessionGrantUncontestedByRow; /** * [ref]:claim 败方读数的分类器 —— **零读**(真相已随 `claimed:false` 回来,384 §4.3-1「settle 自足」), * 把 {@link AskTerminalClaimCurrent} 翻译成 {@link RecoveryRead} 五态。两处消费 —— `resolveLocalOutcome` * 主阶梯与三臂的迟到回调(迟到输同样自足,律 3)—— 共用这**一份**映射(`replayTerminalAskRow` 仍是唯一的 * 三态行映射,这里只把它接到结算动作上)。 * * 沿革:[ref] 时代这里是恢复读口 `readRowForRecovery`(一次带墙钟的 `getAsk`,七态含 pending/unreadable), * 前身 `readDecidedForRecovery`([ref] codex R3-F1)。「CAS 与读真相是两次店往返」= [ref]-R1 残余窗的病根 * ([ref]①):CAS 迟到干净输时真相不在答案里,补读又撞店不可用 ⇒ 人类决议丢失。[ref] 把读口整个撤掉: * 争终局与读真相并成店的一次原子 claim,分类器只剩纯函数半场。[ref](行派生 approve 丢编辑)已随 * [ref] 落列收口:`approved` 臂带行上的 `updatedInput`。 * * 行派生 approve 的守卫只剩一道**在这里**施加([ref]:弃单墓碑守卫随 [ref] 落列整删,见文件头 🪦 注): * · 编辑在飞 ⇒ `edit-in-flight`(codex R4-[critical] 同族口:载荷还在别人手里的那一拍按行结算 = 用裸 * `true` 抢在它前面;持有者随后必定自己结算)。 */ /** * [ref] 让路探针(**只读、不争、不定局**):编辑在飞标记在场时 claim 阶梯刻意**不重打** claim(顶注:本臂 * 抢赢会把持有者正在提交的带编辑批准翻成 park),但一次**已落盘**的决议不该等到标记放开才被消费 —— * codex [ref] R3-F1 的真窗:持有者回读慢于让路预算而标记仍在看门狗内 ⇒ 让路用尽走 park = 把已落盘批准 * 翻成 unavailable。探针 = 一次带墙钟的 `getAsk`;它**不是**败方真相的来源(那是 claim 答案,律 1), * 只在 claim 被扣住的那几拍上不干扰地看一眼行:终态 ⇒ 交分类器(带载荷 approve 当拍兑现 / 负终态、 * PARKING 当拍结算 / 裸 approve 继续让);pending、行不在、读不出 ⇒ `undefined`,继续让路(有界)。 * 它只能**提前**兑现一个已落的决议,永远不会按意图定局 —— 这是它与 [ref] 恢复读口的本质差别。 */ private probeRowWhileYielding; private classifyClaimLoss; /** [ref] codex R3-F2:同一 askId 下**其余本地注册**(重复注册,见 {@link pendingByAskId} 顶注)按 * **park 路由**收尾 —— 行已 PARKING,它们看的是同一条行,终局必须与赢家同形(`unattendedPolicy` * 由各自的闭包读同一个口,deny 部署上一起是 deny)。赢家自己此刻已 settle 过、已从集合里摘除, * 故对它重复调用天然无操作。 */ /** * [ref](案A)+ codex R2-[high]:同一条 askId 下,除 `self` 之外**是否还有悬挂登记在候**。 * * 唯一消费者 = 投递面坍塌的两条臂(取消/断连的 `runCancel`、emit 全灭)。它们的语义是「这条连接没了」, * 而悬挂登记的可达性从不建立在任何连接上 —— 判据与理由全文写在 `runCancel` 的调用点注里(单一属主)。 * 只看 `suspended === true` 且未结算的条目:两条**都有连接**的登记之间的旧语义不在本判据射程内。 */ private hasOtherSuspendedOwner; private settleSameAskIdParkRoute; /** [ref]:落一条 park 墓碑(容量帽按**插入序**逐出最旧,与 `allowAllSessions` 同款有界纪律)。 * 调用点唯一 = `settle` 里的 park 路由支(见那处注);真终局绝不调它。 * * 🔴 **同键第二次调用 = 整个无操作**([ref] 二轮重扫,双 opus confirmed 的修法后半;红先钉 * `test/parked-late-decision.test.ts` 的 [ref]-④)。键换成 wire `approvalId` 之后,「同一把 * approvalId 落两次墓碑」从不可能变成**常态**(同 askId 的重复本地注册各自按 park 路由收尾)。 * 两件事因此必须同时成立: * · **不占第二格、不动插入序** —— 帽与逐出序是按「对外可见的 ask」记的;兄弟刷新插入序会把一条 * 早该轮到逐出的墓碑一次次续命,同样是记账失真。 * · **不覆写既有条目** —— `redeemed`/`inflight` 是本副本**已经对客户端做出的受理承诺**(见 * {@link ParkTombstone});覆写等于把一次已受理的决议重新打开,200 丢包后的重试会去动一张 * **已被消费**的 checkpoint(409/404),与本口承诺的「幂等回放首决」相反。 * 代价如实登记:`parkedAtMs` 保留**首次** park 的时刻 ⇒ TTL 从首次起算(晚到的兄弟不给续期)。 * 方向安全(墓碑早退 = 回落修前的 404,不会错兑),且与「一只 ask 一格」的记账口径自洽。 */ private recordParkTombstone; /** [ref]:取墓碑(**惰性清扫**:过期条目读到即删,不另起定时器 —— 这张表只在迟到回决那一刻被读)。 * 属主门在调用方(与 live 腿同一句判据:`owner === null` 或逐字相等,否则按不存在处置)。 */ private lookupParkTombstone; /** * [ref] 车4 §12-E(F29/F30 裁定形):外部回决(车4 端点或本类 respond 腿)**赢下持久 CAS 之后**,把同 * askId 下全部本地悬挂条目按**真实决议**结算——端点已是唯一权威(CAS 已落),本地只是同步终局;查无 * 条目(跨副本/无流内窗)= 正常,返回 `{ settled: 0 }`,调用方据此诚实回显 `updatedInputForwarded`。 * * 🔴 与 {@link settleVoidedSiblings} 是**两个语义,禁合并**(F29):那边是撤卡收尾,结算值恒 * `(false, "expired")`——批内兄弟被原子撤卡,它们的终局就是「没了」;这边是「同一只 askId 已经有了 * 真决议」,重复注册必须拿到**同一个**决议(approve 就是 approve)——合并会把外部批准的重复条目错 * 结算成拒绝。已被结算过的条目再调 settle 天然无操作(幂等),所以本口对「赢家自己已 settle」安全。 */ notifyExternalDecision(askId: string, allowed: boolean, updatedInput?: unknown): { settled: number; }; /** * [ref] 车6:把一张**批级撤卡帧**投给给定的一组连接(逐 ctx 分发 —— 见 `ApprovalRevokeFrame` 顶注: * [ref] 起 durable 腿的钩子把帧落进本腿账本、sync 腿仍 live;丢帧的结构补偿是重连 preamble 的 * 全量对账基准)。 * * 空名单不发(零信息的帧只会让壳多一次无意义的对账)。每路独立 catch:一路 emit 失败(连接刚死、 * durable-append 目标抖动)**绝不回滚已经落定的持久 CAS** —— 帧是通知,行才是真源。[ref] 的 durable * append 失败同落这只 catch(不新铸失败臂;账本行 VOID 是真源,撤帧丢失由 preamble 对账补偿)。 */ private emitRevokeTo; /** * [ref] 车6 发射点③④:**进程外**收敛器(reaper 腿的 `approval-reconciler.ts`)产出的撤卡帧的投递口。 * 收敛器没有 ctx —— 它只有行上的 (owner, sessionId, taskId),经 broker 反查该身份下**当前**的活跃 * 连接集合。查无活连接 = 正常(壳不在线;重连 preamble 会把这张卡对账掉),返回 0。 * * 两把 key 都试(session 形 + adhoc 形)并去重:行上的 `sessionId` 对 adhoc 腿折的是 taskId * (车2 落行时的折叠),而 broker 的 adhoc 分桶键用的正是 taskId —— 两形各试一次比在这里猜哪种腿 * 更诚实,代价是一次 Map 查找。 */ emitRevoke(frame: ApprovalRevokeFrame, target: { owner: string | null; sessionId: string; taskId: string; }): number; /** Broker key:宿主 session 优先(retained/续聊子代跨 SSE 找得到新流),sessionless adhoc 退 taskId。 * 并发审查加固:显式维度前缀(`"session"`/`"adhoc"`)分隔两个取值域,不再靠 `task:` 字符串前缀"祈祷" * 不会跟一个真实 sessionId 字面重合——此前形式 `sessionId ?? "task:"+taskId` 里,若某会话的 * sessionId 恰好等于另一条 adhoc 流的 `"task:"+taskId`,两者会算出同一把 key(现网因 taskId 由 * 服务端 uuidv7 铸造、请求到达前不可预知而几乎不可达,但维度未加标签是编码层面的真实缺口)。 */ private streamKey; /** Run `fn` with the per-run approval context ambient. On exit, notify every pending broadcast ask that this * ctx is no longer a live target — an ask only dies once ALL its targets are gone (fail-closed backstop, the * inverse of question's release-unanswered — but no longer a blunt "any one target exits ⇒ kill it" rule, * see {@link PendingApproval.onTargetGone}). * [ref] 注意([ref] core 点名的火):委派子代的 ask 经 {@link boundAsk} 落 pending 时不带 * `targetCtxs`/`onTargetGone`(只有 `originTaskId === undefined` 的宿主自身条目才带)——session-scoped * bg 子代活过宿主 turn 时其未决卡不被这里触碰(TTL/abortSignal 仍兜底)。 */ runWithContext(ctx: ToolApprovalRunContext, fn: () => Promise): Promise; /** 测试/可观测性钩子:该 (owner, session/taskId) broker key 下当前活跃连接数——断言多连接注册/清扫 * 行为时用,免得伸手进私有内部状态。 */ liveConnectionCount(owner: string | null, sessionId: string | undefined, taskId: string): number; /** `RunnerDeps.onAsk`. core's resolveAsk calls this for each policy `ask` on the sync leg. [ref] G1 三值化 * (core 1.295):boolean = 人的决定;`"unavailable"` = 本 ask 到达时刻判无活人可同步送达 —— core 以 * approverUnavailable 回路把这只 ask 交回 suspendAsk 走 durable park(park 设施缺席的部署 core 自己 * fail-closed deny)。1.199 的「durable 部署不 wire onAsk」止血就此撤除:恒 wire,park 与 live 卡两全。 * Arrow property so it can be passed as `onAsk: coordinator.ask` with `this` bound. */ ask: (req: AskRequest, signal?: AbortSignal) => Promise; /** [ref] server 半场(审批链战役,[ref] 定谳断点①的修;[ref] HIGH-1 broker 形):宿主 sync * streaming 腿在装配点以本闭包挂 `spec.onAsk`;core 继承链(parentConstraints 冻结 * `spec.onAsk ?? deps.onAsk`,1.294 起)把它逐 ancestor 冻给每个委派子代——子代(bg/嵌套孙代)的 * 权限 ask 不再依赖 ALS,直达宿主 live 流的审批卡。**闭包只携身份**(owner/taskId/sessionId), * emit 时经 broker 取该 (owner, session) 的「当前」活跃流(宿主重连/续聊的新 SSE 自动接卡; * internalsSnapshot 冻结的闭包因此永不携死流)。查无活流 = "unavailable"(G1 回路:durable 部署 * park;inherited plain-ask 的 park 回路 = core [ref]①,落地前该臂 = deny,与 1.257 前行为一致)。 */ boundAsk: (identity: { owner: string | null; taskId: string; sessionId?: string; legKey?: string; /** [ref] 车3 刀 3b(§7.3):本腿的 walltime deadline —— 与 `legKey` 同源同理由,随**出处**走 * (子代经继承链拿到的是宿主腿的闭包,窗必须按宿主腿的剩余 walltime 算)。 */ legDeadlineMonotonic?: number; }) => ((req: AskRequest, signal?: AbortSignal) => Promise); /** 广播版裁决路径——`ask()`(ALS,恒单元素数组)与 `boundAsk`([ref]四多活集合,可能多元素)共用。 * 全体 `ctxs` 保证同一 (owner, sessionId)(streamKey 分组不变式;单元素数组平凡成立)。 * * [ref]三 emit-race 加固(回黑板 [ref]三1,core 确认方向):「等 emit 完成」与「等 * TTL/abort/respond 三选一落定」显式 race——`emit` 的类型契约允许异步实现(durable-append 目标), * 若挂死,旧形 `await ctx.emit(...)` 会让整条 ask 乃至调用方 turn 一起卡死,TTL/abort 定时器虽已 * 触发也无人读取其结果。核心不变式:race 由非人类结局(TTL/abort)先赢、且 emit 广播尚未有任何一路 * 确认完成时,不可扣一个武断的「deny」终局(不知道卡是否曾送达任何人)——按 G1 回路交 * "unavailable"。人类 respond() 落地的裁决永远照旧信任、不受本判据影响:它只可能在 approvalId 已经 * 送达某个客户端之后发生(respond 需要客户端手上有这个 id),这是 outcome==="allowed"/"denied" 与 * "expired"(TTL/abort/run-exit-cleanup/emit 广播全灭四类共用)的天然分野。 */ private askBroadcast; /** `POST /v1/tool-approvals/:id/respond` — settle a parked approval with the shell's decision. Owner-gated with a * 404 (no existence oracle), body validated first (400 is existence-independent) — question/steer parity. The HTTP * layer owns auth (gatedPrincipal + REQUIRE_PRINCIPAL) before calling. * * [ref] 车2(D1/D2):return type 是 UNION,不是把整个方法标 `async`——askStore 缺席时这个方法逐字同步 * 完成(D1 现行为不变;现存量测试对它的同步断言/`void` 调用零改动)。在场时才真的变成 Promise(D2 的 * 「settle 前先赢一把 decideAsk CAS」离不开 await,同步函数做不到)。唯一生产调用点(`routes/runs.ts`) * 统一 `await`——`await` 对非 Promise 值是恒等操作,两条路径对它透明。 */ respond(id: string, principal: string | undefined, body: unknown, /** [ref]:发起本次回决的真实 HTTP 请求。**只**沿 PARKED 臂透传给赎回席(那条腿要它做准入解析与 * 舰队 scope,与 `/decide` 腿同姿势);live 命中路径一个字节都不看它。缺席 = 非 HTTP 调用点 * (测试/内部),赎回席按「无请求」形处置。 */ httpReq?: IncomingMessage): { status: number; body: unknown; } | Promise<{ status: number; body: unknown; }>; /** * [ref]:自由文本规则的**裁决前**文本预检 —— 回 `undefined` = 放行(继续正常回决),回一个响应 = 400。 * * 🔴 **触发条件与兑付口的前置门逐条同源**,一个都不许多(这是本方法最容易写错的地方):只有在这次回决 * **真的会去兑付一条自由文本规则**时才预检。任何一条前置门本来就会把兑付挡掉的形(治理档 ask / * 车道不供 / 无属主 / ctrl+g 编辑放行),都**必须**照旧走「裁决落定 + `rulePersisted:false` + 各自的 * 拒因词」那条老路 —— 否则一次带着坏规则文本的**治理档回决**会被 400 掉,而它本来是能决的:那是把一个 * 附带愿望的失败升级成主动作的失败,方向完全错。逐门对照见 {@link persistRuleAfterDecision}。 * * 候选臂(`edited !== true`)不进这道门:它的等式是「文本必须在引擎铸的候选里」(`rule_not_offered`), * 那是记录级判据、不是文本级判据,`precheckEditedRuleText` 答不了也不该答。 */ private precheckEditedRuleBeforeDecision; /** [ref]:回执上两个新键的**唯一**成形口 —— 只在这次请求真的走了**自由文本臂**时铸。 * 候选臂的回执因此逐字节不变(老壳按固定键集解析,多一个键就是一次没人要的 wire 变更)。 */ private editedArmEcho; /** [ref]:`tool_approval.not_pending` 的**唯一**成形口。文案逐字不变(存量壳/SDK 按它认这条 404), * 只 additive 加判别位;`cause` 缺席 = 这台部署没有行可判(D1 无店)或行形不可信(见各调用点)。 */ private notPending; /** * [ref] —— live 未命中之后的**状态感知分派**(设计 §三的那张表)。 * * 门两道,都在读行之前:①持久店在场(协议上场才有行可读;缺席 ⇒ D1 体逐字不变);②本副本留过 * park 墓碑且属主门过(墓碑是 wire id ↔ 店内 askId 的唯一桥,见 {@link ParkTombstone})。任一不过 ⇒ * 与修前逐字同形的 404(店在场时带 `unknown_or_other_replica` 判别位 —— 那正是「换副本/换端点重试才 * 有意义」的那一因)。 */ private respondLate; /** {@link respondLate} 的读行 + 六态穷举分派(闭集:新增 `AskState` 而不在此表态 = **编译错**)。 */ private dispatchLateByRow; /** [ref]:迟到腿上的 `persistRule` 兑付面 —— 本地条目(规则车道素材的载体)早已随 park 路由散场, * 没有任何可对的候选表 ⇒ 如实拒(裁决本身照常受理,与 {@link persistRuleAfterDecision} 的 * 「失败不翻转裁决」同判)。用既有词 `rule_lane_unavailable`,不为这一支新造词。 */ private lateRuleRefusal; /** * [ref] G2 —— DECIDED 行的**幂等回放**([ref] 锚③)。 * * 同向迟到答与**反向**迟到答同判:都是 200 回放**首决**。反向不是 409、不是报错、更不改判 —— 那是 * 一个 CAS 败者的诚实答:决议早已由首决落定,这次请求只是晚到,响应体把首决交回去,消费端自判向。 * * 权威性四合取与 durable 回决腿的 `decidedRowIsAuthoritative` **同判据**(那处顶注是唯一真源): * 决议二值 ∧ 非 provisional ∧ 有 `decidedAtMs` ∧ 态确为 DECIDED。不合形 ⇒ 退回修前 404(绝不把一条 * 坏行包装成「人的首决」交出去);此处比那条腿更保守地不铸 500,理由是本腿的客户端是壳上那张卡, * 它对 404 有成文处置(消卡),对 500 没有。 */ private replayDecidedAsk; /** * [ref] G1 —— PARKED 行的迟到决议:**桥接既有赎回腿**([ref] 锚①「同腿新调用方,零新终局语义」)。 * * 为什么桥接而不是在这里 revive-直续:PARKED 的语义是「候下一 run 重呈或 operator 决议」,而 * operator 决议早有成文入口(`/decide` 那条链:checkpoint decide CAS → resume)。桥接 = 那条链多一个 * 调用方,单赢者仍是它的 CAS;直续则要发明第三种赎回形,与 core 的 park/revive 契约重叠。 * * 200 是**受理**语义([ref] 先例):resume 的驱动是 core 内的 fire-and-forget,结果面走 run/events。 * 非 2xx 原样透出(gate 被别人收走 ⇒ 409 等),绝不粉饰成受理。 */ private redeemParkedAsk; /** * [ref] —— **durable 回决腿的规则位**:PARKED 行的迟到决议携 `persistRule` 时,用**行上**的素材兑付。 * * 回 `undefined` = 这次请求没带 `persistRule`(一个键都不加,老壳的回执逐字节不变);否则回一个 * `{rulePersisted, ruleRefusal?, …echo}` 片段,由调用点铺进 200 体 —— 与 live 腿 `withFlag` 的键集 * **同形**(壳的解析器一套判据吃两条腿)。 * * ## 素材从哪来(这是本方法存在的全部理由) * live 腿的等式左边是**进程内条目** `PendingApproval.ruleLane`,而 park 路由把条目连同素材一起散场 * 了([ref] 的墓碑只桥身份,不携素材)。本腿因此从**行**取,一件一件对着 live 腿的入参: * · `command` = `rule_command` 列(**[ref] 新列**;缺席即本方法唯一的新拒因,见下); * · `scope` = `rule_scope_root` 列 ⇒ `{kind:"project", root}`,缺席则**不铸 scope**(core 落 global, * = live 腿 `material.scopeRoot` 缺席时逐字同形 —— 绝不在坐标系不明时编一个 root); * · 候选表 / `toolName` / 治理位 = 行上的 `card_json`(呈卡帧的同一份;**人看见的就是它**)。 * ⚠️ 卡上的 `toolName` 带 200 字符的投影帽(`approval-card.ts` 的 `MAX_IDENT`),理论上可被截。 * **那一支结构性 fail-closed、不会静默存错规则**:规则文本由 `toolName` + 命令派生,截过的名字 * 让 core 重铸出**另一条**规则 ⇒ 候选臂的文本等式当场不成立(`unknown-candidate`),编辑臂则被 * core 自己的覆盖门拒。现网不可达(canonical / `mcp__srv__tool` 都远短于 200),记在这里免得 * 下一个人以为是漏了; * · `toolCallId` / `boundInputHash` = 行上的同名列(与 live 腿铸素材时读的是同一个 `req` 的同两件)。 * * ## 三道门与 live 腿逐条同源(顺序也同,理由逐字见 `persistRuleAfterDecision`) * 治理档先判(否则一只治理 ask 会拿到「这台部署不供规则」这个更宽的词)→ 车道/属主 → 素材 → * `updatedInput`(ctrl+g 编辑放行 ⇒ `rule_input_edited`:生效的是编辑后的调用,而候选是从**原始** * 命令铸的)→ 候选臂的文本等式。 * * ## 与 live 腿**刻意不同**的一处:没有裁决前的自由文本预检([ref] 的那道 400) * 那道门的语义是「拒的时候这张卡还在,改好再来」;而本腿走到这里时赎回腿的 CAS **已经赢了** * (checkpoint 已被消费),卡不可能还在 —— 400 回去只会把一次**已经生效**的放行谎报成失败。 * 所以编辑臂在本腿上照旧落「裁决受理 + `rulePersisted:false` + `edit-rejected`」那条老路(= [ref] * 之前 live 腿的行为),如实登记为本腿的残余。 */ private persistRuleFromRow; /** [ref]:回执/单飞胜者的**回放形** —— 与首次受理同键集,多一位 `idempotent`,`decision` 恒是**受理时** * 那一向(反向重试不改判,与 DECIDED 臂同一条纪律)。 */ private replayRedeemReceipt; /** * [ref] 车二:回决携规则确认 ⇒ prepare→confirm→redeem(实现在 `rules-consent.ts`,本方法只做门与回显)。 * * 三道门,每道都是**拒绝**而不是降级: * ① 裁决必须真落定(200)—— 见调用点注; * ② 必须有已验明的 owner —— 规则是**某个人**的持久同意,一条 `owner === null` 的 ask 没有归属人可写; * ③ 本 ask 必须登记过规则车道素材(店在场 ∧ 引擎铸过候选 ∧ 命令可读)。 * * 失败**不翻转裁决**:人的「允许这次」已经生效并且已经交给引擎了,把整个回决改判成 4xx 会让一次真实的 * 放行凭空消失。诚实的形是 200 + `rulePersisted:false`(壳据它告诉人「这次放行了,但『不再询问』没存上」)。 */ private persistRuleAfterDecision; /** store 在场时的回决 CAS 门(D2)——`respond()` 的异步分支,拆出以保持 `respond()` 本身在 store 缺席 * 时仍是纯同步函数(D1)。`askStore`/`askId`/`batchId` 由调用方在已窄化的分支里传入(避免非空断言)。 */ private respondWithCas; /** 回决收尾(D1 现行为逐字不变的那一半)——session allow-all 记账 + settle + 200 响应体。原 `respond()` * 方法体的逐字搬运([ref] 车2 拆分,行为零改动)。 */ private finishRespond; /** [ref]([ref] 案):live pending 的**列表读面**——`GET /v1/approvals` 响应第二顶层键 `livePending` * 的唯一数据源,**同时**是 `/v1/approvals/stream` 的 live 半场 diff 源(推与拉必须同源:feed 收到 * `live_pending` 事件就会去 `list()`,两处各读各的会给出一条列不出来的事件)。纯读投影,零锁/零结算 * 语义。 * 🔴 **S-454 起行上带整只活卡帧**(`frame`,见 {@link LivePendingRow.frame}):旧文那句「input/args * 不上列表,详情走流帧」对**悬挂 ask 结构上不成立**(它永远没有流帧),而 `frame` 是 `askBroadcast` * 那一处构造的同一个对象 ⇒ 仍然只有**一个**脱敏属主、零第二投影。`scope` 与 durable 面同一表达式:`undefined` = operator 全量(含 owner null 的 * 条目),串 = 只见 `owner === scope` 的条目(null-owner 行对 principal 不可见,fail-closed—— * 宁少列不超范围;`"__none__"` 未鉴权哨兵自然恒空)。行的 `approvalId` 就是 `respond()` 收的 id * (同源可达,[ref] 判据);`expiresAtMs` 照抄登记时单点铸定的窗死线([ref] 三键同一个数)。 */ listLivePending(scope: string | undefined): LivePendingRow[]; /** * 🔴 [ref](案A)追记 F4 —— **跨副本决议的执行通知**(悬挂行的一次终态轮询)。 * * ## 缺口的准确形状(别照抄第一版叙述) * 案A 的**发现**路径经 durable 腿本就跨副本可达:悬挂 ask 落的是共享店里的 `STREAM_PENDING` 行, * `GET /v1/approvals` 的 durable 段、壳队列、任一副本的 decide 端点都读得到它(codex 那句「另一副本 * 列不出」经亲验为错述 —— 那说的是 `listLivePending`,它只是 live 腿)。真缺口在**执行**侧:兑现这次 * 决议的 promise 是**进程内**的,只活在铸卡的那个副本里。人的决议打到副本 B ⇒ 行被 CAS 成 `DECIDED`, * 而副本 A 上那条悬挂 promise 没有任何本地事件会驱动它 —— 挂到窗满(1h)才收场,人已经批了。 * * ## 二选一的取舍(设计稿追记 F4 要求写死) * 候选①**副本亲和路由**(把 decide 请求转发到持有 promise 的副本):需要一张「谁持有哪只 ask」的跨副本 * 注册表 + 它自己的过期/裂脑收敛,而那张表的真源仍然是这张 ask 行 —— 等于为一个通知问题引入第二套一致性 * 问题;且亲和一旦失效(副本刚死),决议就**没有**回落路径。 * 候选②**原副本轮询唤醒**(本实现):不新开任何定时器族,搭 reaper 既有节拍(`boot/reapers.ts` 的 * tick,与对账收敛器同一拍);读的是**行本身**这一个真源;副本死了就没有 promise 可唤醒,那与「run * 已经死了」是同一件事,不需要额外语义。**裁定 = ②**,理由一句话:通知面不该比它通知的那件事更复杂。 * 代价如实记:唤醒延迟上界 = 一个 reaper tick(默认 `REAP_INTERVAL_SEC`),不是毫秒级 —— 悬挂 ask 的 * 时间尺度是分钟到小时,这个上界在语义上是免费的;真要秒级,那是亲和路由的活,登记为后续件。 * * ## 判据(逐条窄) * · **只轮询悬挂条目**(`suspended === true` ∧ 有 `askId` ∧ 未结算):有活流的 ask 有自己的 * emit/respond/窗三条路,轮询它们只会在既有竞争者之外多加一个写者; * · **只读不写**:一次 `getAsk` 点读(带墙钟 deadline),读失败 ⇒ 记账 + 本轮跳过(下轮再来), * 绝不因为读不出来就替它编终局; * · **行仍 `STREAM_PENDING` ⇒ 什么都不做**(那是悬挂的**期望态**,不是异常); * · 终态映射复用 {@link replayTerminalAskRow}(不造第二套):`DECIDED` ⇒ 真决议经 * {@link notifyExternalDecision}(它对已结算条目天然幂等);`PARKING|PARKED` ⇒ park 路由口 * {@link settleSameAskIdParkRoute};`DENIED|VOID` ⇒ 裸 deny(同 {@link settleVoidedSiblings} 的形)。 * · 同一 askId 只读一次(多条本地注册共享一次点读)。 * * 返回 `{ polled, settled, backlog, truncated }` —— `polled` = 本轮真去店里点读的 askId 数,`settled` = * 被唤醒的**本地条目**数,`backlog` = 本拍收尾时**还没轮到**的悬挂候选剩余数(`candidates.length - * polled` 下界 0),`truncated` = 帽/预算截断位(F4;截断同拍响亮 warn,键同名同值)。 */ pollSuspendedDecisions(): Promise<{ polled: number; settled: number; backlog: number; truncated: boolean; }>; /** * 🔴 [ref] + codex R3-[critical](真缺陷,红先复现器在 `test/case-a-reachability-queue.test.ts` 同名格) * —— **带编辑的决议在飞期间,观察者不结算**。 * * 病灶:`decideAsk` 是「提交 → **另起**一次回读」的形。带 `updatedInput` 的请求 W 赢下 CAS 之后、 * 回读返回**之前**,任何并发请求 R 看到的就是一条 `DECIDED` 行 —— 它经 `syncLiveFromDecidedRow` 用 * **行派生**的裸 `true` 把本地等待腿结算掉(那条腿从此消失);W 的回读回来要转发编辑时 `settled` 已是 0。 * 人批的是改好的命令,跑的是原来那条 —— 与 census 第 31 行同一种伤害,只是成因是**进程内的竞态**。 * * 为什么进程内可解(这决定了它归本车而不是归那条店语义级欠账):这场竞态**恒在同一副本内** —— * W 能过决议口 CAS 前那道门,前提就是「本副本有活体等待腿」;R 要真伤到人,也必须结算**同一条**本地腿。 * * 形:**只让路,不猜载荷**。在飞标记只回答「此刻有没有另一次请求正带着编辑决这只 ask」;观察者据它 * **跳过结算**(把结算权留给持有编辑的那一次),绝不去替它转发一份自己没见过的载荷 —— 猜载荷就会在 * 「两个并发 approve、一个带编辑一个不带」的形上执行一个**不是赢家**的决议。 * 让路是安全的:标记的持有者在 `decideAsk` 返回后**必定**走两条路之一(赢 ⇒ 带编辑结算;输 ⇒ 经 * `syncLiveFromDecidedRow` 按行结算),而那时标记已经释放 ⇒ 不存在「谁都不结算」的窗口。 * * 生命周期硬条款:`mark` 与它返回的 `release` 必须**只包住 `decideAsk` 这一次调用**(try/finally), * 不包住调用之后的结算与响应 —— 包多了就把自己的结算路也让掉,那才是真的挂死。 */ markEditedDecisionInFlight(askId: string): () => void; /** {@link markEditedDecisionInFlight} 的**并发计数**读口:此刻有几次请求正带着编辑决这只 ask。 * 提交歧义臂的认领判据要它 —— 见 `routes/runs.ts` 那段注:`> 1` 时「谁的载荷是赢家」无从证明。 */ editedDecisionInFlightCount(askId: string): number; /** {@link markEditedDecisionInFlight} 的读口:此刻有没有**别的**请求正带着编辑决这只 ask。 */ editedDecisionInFlight(askId: string): boolean; /** 判据钩子:把「看门狗那一拍」显式地跑一次(= 在飞标记被放弃归零;[ref] 起看门狗不再铸弃单墓碑, * 本钩子随之只剩这一半)。存在理由 = 那条臂的真实触发要等 {@link EDITED_DECISION_MARK_CAP_MS} 的 * 墙钟,而本仓禁固定睡眠钉(fixed-sleep 棘轮)。生产路径不调它。 */ expireEditedDecisionMarkForTest(askId: string): void; /** Test/observability hooks. */ pendingCount(): number; /** [ref]:当前挂着的**悬挂**条目数(零投递集合铸的那一类)——运维/判据读它证「悬挂队列有多长」。 */ suspendedCount(): number; sessionAllowedCount(): number; /** [ref]:当前在场的 park 墓碑数(有界性的可观测面 —— 判据据它证「帽是硬的、过期会被清」)。 */ parkTombstoneCount(): number; } //# sourceMappingURL=tool-approval.d.ts.map