@sema-agent/server 7.4.0 → 7.6.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/LICENSE +1 -1
- package/README.md +18 -3
- package/README.zh-CN.md +14 -3
- package/USAGE.md +80 -1
- package/dist/approval-card.d.ts +15 -3
- package/dist/approval-card.js +41 -7
- package/dist/approval-reconciler.d.ts +109 -12
- package/dist/approval-reconciler.js +152 -24
- package/dist/boot/config-center.js +15 -2
- package/dist/boot/coordinators.js +10 -2
- package/dist/boot/execution-env.js +1 -1
- package/dist/boot/org-memory.d.ts +6 -0
- package/dist/boot/org-memory.js +1 -1
- package/dist/boot/parked-revive-gate.d.ts +78 -0
- package/dist/boot/parked-revive-gate.js +114 -0
- package/dist/boot/reapers.d.ts +2 -0
- package/dist/boot/reapers.js +11 -4
- package/dist/boot/resolve-spec.d.ts +3 -19
- package/dist/boot/resolve-spec.js +73 -67
- package/dist/boot/runner-deps.d.ts +23 -1
- package/dist/boot/runner-deps.js +8 -11
- package/dist/boot/workflow-orchestration.d.ts +8 -3
- package/dist/boot/workflow-orchestration.js +23 -1
- package/dist/budget.d.ts +1 -1
- package/dist/budget.js +1 -1
- package/dist/capabilities/repo-tools.d.ts +1 -1
- package/dist/capabilities/repo-tools.js +8 -2
- package/dist/config-center/apply-effective.js +33 -10
- package/dist/config-provider.d.ts +1 -0
- package/dist/config-provider.js +23 -3
- package/dist/config-types.d.ts +27 -9
- package/dist/config.d.ts +6 -1
- package/dist/config.js +61 -15
- package/dist/deployment-governance.d.ts +168 -0
- package/dist/deployment-governance.js +206 -0
- package/dist/env-facts.d.ts +3 -1
- package/dist/env-facts.js +3 -1
- package/dist/fleet/fleet-bus.d.ts +17 -2
- package/dist/fleet/fleet-bus.js +68 -3
- package/dist/fleet/fleet-terminal-window.d.ts +98 -0
- package/dist/fleet/fleet-terminal-window.js +316 -0
- package/dist/governance-ask-marks.d.ts +31 -0
- package/dist/governance-ask-marks.js +122 -0
- package/dist/hooks/hook-runner.d.ts +28 -0
- package/dist/hooks/hook-runner.js +149 -25
- package/dist/http/routes/approvals-assistant.js +6 -7
- package/dist/http/routes/diagnostics.js +10 -5
- package/dist/http/routes/fleet.js +160 -14
- package/dist/http/routes/memory-policy.d.ts +2 -1
- package/dist/http/routes/memory-policy.js +77 -13
- package/dist/http/routes/runs.js +6 -2
- package/dist/http/routes/tasks.js +59 -22
- package/dist/http/routes/trace-usage.js +3 -4
- package/dist/http/send.d.ts +23 -0
- package/dist/http/send.js +23 -0
- package/dist/http/server.d.ts +9 -0
- package/dist/http/server.js +28 -14
- package/dist/http/sse-log.js +3 -4
- package/dist/http/wire-types.d.ts +7 -2
- package/dist/leader/diffout.d.ts +10 -0
- package/dist/leader/diffout.js +14 -2
- package/dist/leader/diffup.js +3 -2
- package/dist/leader/planner.js +7 -0
- package/dist/main.js +39 -31
- package/dist/observability/fail-open.d.ts +17 -2
- package/dist/observability/fail-open.js +19 -4
- package/dist/observability/prompt-manifest.d.ts +5 -1
- package/dist/orchestration/workflow-notify-journal.d.ts +58 -2
- package/dist/orchestration/workflow-notify-journal.js +130 -45
- package/dist/parked-decide.js +9 -4
- package/dist/plugins/approval-ask-store-memory.d.ts +2 -2
- package/dist/plugins/approval-ask-store-memory.js +3 -2
- package/dist/plugins/approval-ask-store-sql.d.ts +60 -5
- package/dist/plugins/approval-ask-store-sql.js +75 -35
- package/dist/plugins/background-agent-store-sql.js +16 -16
- package/dist/plugins/background-shell-support.d.ts +1 -1
- package/dist/plugins/background-shell-support.js +2 -2
- package/dist/plugins/breaker-state-sql.js +2 -2
- package/dist/plugins/checkpoint-store-sql.d.ts +67 -8
- package/dist/plugins/checkpoint-store-sql.js +76 -13
- package/dist/plugins/image-bake-store-sql.d.ts +1 -1
- package/dist/plugins/image-bake-store-sql.js +27 -27
- package/dist/plugins/image-index-sql.js +15 -15
- package/dist/plugins/local-checkpoint-store.d.ts +20 -1
- package/dist/plugins/local-checkpoint-store.js +19 -0
- package/dist/plugins/mailbox-store-sql.d.ts +4 -10
- package/dist/plugins/mailbox-store-sql.js +59 -6
- package/dist/plugins/memory-engine-pg.js +9 -9
- package/dist/plugins/memory-engine-tidb.js +7 -7
- package/dist/plugins/memory-sync-store-pg.js +13 -13
- package/dist/plugins/memory-sync-store-tidb.js +5 -5
- package/dist/plugins/outcome-ledger-sql.js +7 -7
- package/dist/plugins/pg-cost-quota.js +3 -3
- package/dist/plugins/pg-pool.js +84 -75
- package/dist/plugins/pg-rate-limiter.js +3 -3
- package/dist/plugins/pg-session-storage.d.ts +1 -1
- package/dist/plugins/pg-session-storage.js +12 -13
- package/dist/plugins/remote-env-host.js +3 -1
- package/dist/plugins/remote-env-local-docker.js +6 -3
- package/dist/plugins/remote-env-ssh.d.ts +13 -1
- package/dist/plugins/roster-store-sql.js +8 -8
- package/dist/plugins/store-contracts.d.ts +19 -0
- package/dist/plugins/store-contracts.js +42 -0
- package/dist/plugins/task-attachment-store.js +5 -5
- package/dist/plugins/task-list-store-sql.js +1 -1
- package/dist/plugins/tidb-cost-quota.js +1 -1
- package/dist/plugins/tidb-pool.js +83 -60
- package/dist/plugins/tidb-rate-limiter.js +1 -1
- package/dist/plugins/tidb-session-store.js +2 -5
- package/dist/plugins/tool-result-store-sql.js +2 -2
- package/dist/plugins/usage-window-store-sql.js +13 -13
- package/dist/plugins/write-behind-counter.d.ts +10 -2
- package/dist/plugins/write-behind-counter.js +13 -3
- package/dist/resource-suspend.d.ts +3 -1
- package/dist/resource-suspend.js +3 -1
- package/dist/run-local.d.ts +73 -1
- package/dist/run-local.js +146 -5
- package/dist/runs.d.ts +11 -1
- package/dist/runs.js +18 -3
- package/dist/runtime-governance.d.ts +18 -0
- package/dist/runtime-governance.js +90 -3
- package/dist/security.d.ts +12 -0
- package/dist/security.js +12 -0
- package/dist/session-sync-kernel.d.ts +13 -0
- package/dist/session-sync-kernel.js +13 -0
- package/dist/task-settings.d.ts +3 -9
- package/dist/task-settings.js +16 -13
- package/dist/tool-approval.d.ts +33 -6
- package/dist/tool-approval.js +80 -23
- package/dist/trace/core-keyset-guard.d.ts +18 -4
- package/dist/trace/project.d.ts +10 -1
- package/dist/trace/project.js +31 -0
- package/package.json +3 -3
- package/dist/boot/lexical-path-env.d.ts +0 -10
- package/dist/boot/lexical-path-env.js +0 -88
- package/dist/capabilities/oa-tools.d.ts +0 -15
- package/dist/capabilities/oa-tools.js +0 -54
- package/dist/finance/cost-taxonomy.d.ts +0 -34
- package/dist/finance/cost-taxonomy.js +0 -26
- package/dist/plugins/approval-store-sql.d.ts +0 -116
- package/dist/plugins/approval-store-sql.js +0 -151
- package/dist/plugins/file-workflow-journal-store.d.ts +0 -12
- package/dist/plugins/file-workflow-journal-store.js +0 -12
- package/dist/plugins/pg-approval-store.d.ts +0 -9
- package/dist/plugins/pg-approval-store.js +0 -9
- package/dist/plugins/pg-breaker-state.d.ts +0 -8
- package/dist/plugins/pg-breaker-state.js +0 -8
- package/dist/plugins/pg-checkpoint-store.d.ts +0 -10
- package/dist/plugins/pg-checkpoint-store.js +0 -10
- package/dist/plugins/pg-file-snapshot-store.d.ts +0 -8
- package/dist/plugins/pg-file-snapshot-store.js +0 -8
- package/dist/plugins/pg-image-bake.d.ts +0 -12
- package/dist/plugins/pg-image-bake.js +0 -11
- package/dist/plugins/pg-image-index.d.ts +0 -12
- package/dist/plugins/pg-image-index.js +0 -11
- package/dist/plugins/pg-outcome-ledger.d.ts +0 -12
- package/dist/plugins/pg-outcome-ledger.js +0 -11
- package/dist/plugins/pg-resume-anchor-store.d.ts +0 -7
- package/dist/plugins/pg-resume-anchor-store.js +0 -7
- package/dist/plugins/pg-run-store.d.ts +0 -9
- package/dist/plugins/pg-run-store.js +0 -9
- package/dist/plugins/pg-session-policy-store.d.ts +0 -7
- package/dist/plugins/pg-session-policy-store.js +0 -7
- package/dist/plugins/pg-session-store.d.ts +0 -12
- package/dist/plugins/pg-session-store.js +0 -12
- package/dist/plugins/pg-tool-result-store.d.ts +0 -9
- package/dist/plugins/pg-tool-result-store.js +0 -9
- package/dist/plugins/pg-workflow-journal-store.d.ts +0 -9
- package/dist/plugins/pg-workflow-journal-store.js +0 -9
- package/dist/plugins/pg-workflow-run-store.d.ts +0 -9
- package/dist/plugins/pg-workflow-run-store.js +0 -9
- package/dist/plugins/tidb-approval-store.d.ts +0 -8
- package/dist/plugins/tidb-approval-store.js +0 -8
- package/dist/plugins/tidb-breaker-state.d.ts +0 -7
- package/dist/plugins/tidb-breaker-state.js +0 -7
- package/dist/plugins/tidb-checkpoint-store.d.ts +0 -9
- package/dist/plugins/tidb-checkpoint-store.js +0 -9
- package/dist/plugins/tidb-file-snapshot-store.d.ts +0 -8
- package/dist/plugins/tidb-file-snapshot-store.js +0 -8
- package/dist/plugins/tidb-image-bake.d.ts +0 -12
- package/dist/plugins/tidb-image-bake.js +0 -11
- package/dist/plugins/tidb-image-index.d.ts +0 -12
- package/dist/plugins/tidb-image-index.js +0 -11
- package/dist/plugins/tidb-outcome-ledger.d.ts +0 -12
- package/dist/plugins/tidb-outcome-ledger.js +0 -12
- package/dist/plugins/tidb-resume-anchor-store.d.ts +0 -7
- package/dist/plugins/tidb-resume-anchor-store.js +0 -7
- package/dist/plugins/tidb-run-store.d.ts +0 -10
- package/dist/plugins/tidb-run-store.js +0 -9
- package/dist/plugins/tidb-session-policy-store.d.ts +0 -7
- package/dist/plugins/tidb-session-policy-store.js +0 -7
- package/dist/plugins/tidb-tool-result-store.d.ts +0 -8
- package/dist/plugins/tidb-tool-result-store.js +0 -10
- package/dist/plugins/tidb-workflow-journal-store.d.ts +0 -9
- package/dist/plugins/tidb-workflow-journal-store.js +0 -9
- package/dist/plugins/tidb-workflow-run-store.d.ts +0 -10
- package/dist/plugins/tidb-workflow-run-store.js +0 -10
- package/dist/plugins/workflow-journal-limits.d.ts +0 -12
- package/dist/plugins/workflow-journal-limits.js +0 -12
- package/dist/sema-registry.d.ts +0 -41
- package/dist/sema-registry.js +0 -40
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [2942]/[2943] `governanceForced` 的**判定缝** —— operator 治理层产的 ask 在 server 进程内的标记通道。
|
|
3
|
+
*
|
|
4
|
+
* 🔴 为什么需要一条 out-of-band 通道(侦察结论,亲验装树 core 5.16.x dist):
|
|
5
|
+
* `PermissionResult.decisionReason`(`"rule" | "mode" | "hook" | "safety" | "classifier"`)**到不了帧铸点**。
|
|
6
|
+
* core 在 `dist/core/runner/prepare-task.js` 的 ask 铸造点(`resolveAsk({...})`,三处继承臂 + 主臂)只把
|
|
7
|
+
* `toolName / toolCallId / args / preview / message / askSourceIdentity() / riskAxesOf() / requiresRealApproval`
|
|
8
|
+
* 装进 `AskRequest`;`decisionReason` 连同整个 `PermissionResult` 一起留在 core 内部。`AskRequest` 的类型面
|
|
9
|
+
* (`dist/core/tool-policy.d.ts:78`)也确认没有这个键。所以「映射 decisionReason ⇒ governanceForced」这条
|
|
10
|
+
* 缝在**当前 core 上不存在**——它不是没接线,是没有这个字段可读。
|
|
11
|
+
*
|
|
12
|
+
* 另一侧同样不可用:`riskAxes.irreversible` 在 shellGate 上场时对**每一次** shell 调用都为真(core
|
|
13
|
+
* `prepare-task.js:1620-1631` 无条件 `irreversibilityTier.set("Bash"/"Monitor", …)`),与「这只 ask 是谁
|
|
14
|
+
* 提的」无关;`requiresRealApproval` 的生产者是 core 自己的两条安全策略(unverifiable-delete /
|
|
15
|
+
* transcript-integrity),与治理层无关。两者都不是判据。
|
|
16
|
+
*
|
|
17
|
+
* ⇒ 采用的方案 = **server 标记自家产物**:治理层策略(`runtime-governance.ts` 的
|
|
18
|
+
* `applyRuntimeGovernance`)在合成时被包一层观察器,自己产 `ask` 时把该次调用的 `toolCallId` 记进本表;
|
|
19
|
+
* 帧铸点(`tool-approval.ts` 的 `askBroadcast`)按 `toolCallId` 回查。
|
|
20
|
+
* **禁把信号藏进 message 文本**——那是 in-band hack,消费端只能靠正则猜。
|
|
21
|
+
*
|
|
22
|
+
* ## 三条硬语义
|
|
23
|
+
*
|
|
24
|
+
* 1. **只做正向断言**。命中 = 有直接证据(治理策略本人产的 ask,或治理层把 shellGate 抬到 `"always"` 后
|
|
25
|
+
* 的 shell 调用——该档下每一次 shell 调用都被治理层门住,推断是紧的)。**未命中 ≠ 「不是治理」**,
|
|
26
|
+
* 只是「没有证据」⇒ wire 上键**缺席**,绝不编一个 `false`。宁可漏标,不可错标:一个错标会把运维支去
|
|
27
|
+
* 翻一份根本没门住这只 ask 的治理配置。
|
|
28
|
+
* 2. **peek,不 consume**。同一 `toolCallId` 可能被广播两次(重试 / failover / 闭包再入,见
|
|
29
|
+
* `tool-approval.ts` 里 `pendingByAskId` 的顶注),消费式读会让第二帧莫名丢键。
|
|
30
|
+
* 3. **有界**。写入点是每一次工具调用,读取点只在 ask 上——未被读到的标记(调用被放行 / 被别的策略 deny)
|
|
31
|
+
* 必须能自己老化掉。按插入序淘汰最旧的(`Set` 的迭代序 = 插入序)。
|
|
32
|
+
*
|
|
33
|
+
* ## 作用域:按**审批 broker key** 分格,不是进程级一锅烩
|
|
34
|
+
*
|
|
35
|
+
* 🔴 codex 交叉复审 round3 [medium](验真):`toolCallId` **不是**全局唯一的 —— 它多数情况下是**提供方**
|
|
36
|
+
* 逐字给的 id(core 的 brain 层不另铸),本仓自己的代码就是这么假设的:core 把 tool-result 引用命名成
|
|
37
|
+
* `tr_<sessionId>_<toolCallId>`(见 `plugins/tool-result-store-sql.ts` 的 `deleteBySession` 顶注),
|
|
38
|
+
* `deriveAskId` 也把 `sourceTaskId`/`runId`/`legKey` 一并入哈希。一张不分格的表会**跨任务/跨租户串味**:
|
|
39
|
+
* 低熵 id(本地/自建模型服务常见的 `call_1` 之类)下,A 任务的治理标会被 B 任务的普通 ask 读走(假出处),
|
|
40
|
+
* 或被 B 的一次治理放行反手清掉(丢真出处)。
|
|
41
|
+
*
|
|
42
|
+
* 分格用的 **scope = 审批协调器的 broker key**(`["session", owner, sessionId]` / `["adhoc", owner, taskId]`)。
|
|
43
|
+
* 选它的理由是**两侧都够得到、且是同一个值**:
|
|
44
|
+
* · 写侧(治理策略的观察器跑在 core 的 `policy.check` 里,入参只有 `ToolCallRequest`)⇒ 经
|
|
45
|
+
* AsyncLocalStorage 拿 —— 作用域由 `runWithContext` 与审批 ctx **同拍**进出。
|
|
46
|
+
* · 读侧(`askBroadcast`)⇒ 由 `origin`/`primary` **显式算出**同一把 key。
|
|
47
|
+
*
|
|
48
|
+
* 🔴 读侧**显式算**而不是也读 ALS(codex 交叉复审 round5 [medium],红先复现后改的形):委派/长命 bg 子代
|
|
49
|
+
* 的 ask 可能在**另一条腿**的上下文里发出(宿主腿退场、壳重连出新腿,子代的卡落到新腿的流上)。读侧若取
|
|
50
|
+
* 环境里的那张表,拿到的是**新腿**的空表,信号静默丢失 —— 那条路径已由
|
|
51
|
+
* `test/governance-forced-signal.test.ts` 的「长命 bg 子代形」用例钉住(改形前它是红的)。scope 由**出处**
|
|
52
|
+
* 决定、与卡投递到哪条连接无关,与本仓 `AskOriginIdentity` 顶注「投递面与出处面是两件事」同源。
|
|
53
|
+
*
|
|
54
|
+
* 残余(成文):同一 (owner, sessionId) 下的**并发多 run** 共用一格 —— 那正是 broker 自己的分组粒度
|
|
55
|
+
* (卡本来就跨该会话投递),同租户同会话内的 id 碰撞才会互串,比进程级一锅烩窄得多。要再窄需要一个
|
|
56
|
+
* 写侧也够得到的 run 级身份,core 的 `ToolCallRequest` 今天给不出。
|
|
57
|
+
*/
|
|
58
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
59
|
+
/** 默认表容量(全进程合计,复合键计数)。一次 turn 的工具调用数远小于此;满表淘汰的是最旧的标记
|
|
60
|
+
* (那些早已不可能再被读到)。 */
|
|
61
|
+
const DEFAULT_MAX_MARKS = 1024;
|
|
62
|
+
/** 复合键。JSON 元组编码(同 `streamKey`/`sessionAllowKey` 先例):任何一段里含分隔符都不可能与下一段
|
|
63
|
+
* 的边界产生歧义 —— 一个精心构造的 sessionId 因此无法伪造出另一格。 */
|
|
64
|
+
const compositeKey = (scope, toolCallId) => JSON.stringify([scope, toolCallId]);
|
|
65
|
+
/** 一张独立的标记表(不进程共享)。生产路径用的是下面 `governanceAskMarksFor` 给出的**分格视图**;
|
|
66
|
+
* 本工厂给测试(与将来的多实例形)一个不依赖任何环境的干净表。 */
|
|
67
|
+
export function createGovernanceAskMarks(opts) {
|
|
68
|
+
return createScopedView(new Set(), "", Math.max(1, Math.floor(opts?.max ?? DEFAULT_MAX_MARKS)));
|
|
69
|
+
}
|
|
70
|
+
function createScopedView(marks, scope, max) {
|
|
71
|
+
return {
|
|
72
|
+
setMarked(toolCallId, marked) {
|
|
73
|
+
if (typeof toolCallId !== "string" || toolCallId === "")
|
|
74
|
+
return; // 无身份 ⇒ 无从回查,不占格
|
|
75
|
+
const key = compositeKey(scope, toolCallId);
|
|
76
|
+
// 重标记要**刷新新鲜度**:先删后加把它挪到插入序尾部,免得一只长命的 ask 被自己后来的调用挤掉。
|
|
77
|
+
marks.delete(key);
|
|
78
|
+
if (!marked)
|
|
79
|
+
return; // 覆写成「不是治理门」= 清格(setMarked 顶注:复查路径的陈旧标)
|
|
80
|
+
marks.add(key);
|
|
81
|
+
while (marks.size > max) {
|
|
82
|
+
const oldest = marks.values().next();
|
|
83
|
+
if (oldest.done)
|
|
84
|
+
break;
|
|
85
|
+
marks.delete(oldest.value);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
isMarked(toolCallId) {
|
|
89
|
+
return typeof toolCallId === "string" && toolCallId !== "" && marks.has(compositeKey(scope, toolCallId));
|
|
90
|
+
},
|
|
91
|
+
clear() {
|
|
92
|
+
for (const k of [...marks])
|
|
93
|
+
if (k.startsWith(`[${JSON.stringify(scope)},`))
|
|
94
|
+
marks.delete(k);
|
|
95
|
+
},
|
|
96
|
+
size() {
|
|
97
|
+
let n = 0;
|
|
98
|
+
for (const k of marks)
|
|
99
|
+
if (k.startsWith(`[${JSON.stringify(scope)},`))
|
|
100
|
+
n += 1;
|
|
101
|
+
return n;
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** 进程级底表(分格由复合键承担,见顶注「作用域」段)。 */
|
|
106
|
+
const processMarks = new Set();
|
|
107
|
+
/** 写侧的 scope 载体 —— 由 `ToolApprovalCoordinator.runWithContext` 与审批 ctx 同拍进出。 */
|
|
108
|
+
const scopeAls = new AsyncLocalStorage();
|
|
109
|
+
/** 进入一条 run 腿的标记作用域(唯一调用点 = `ToolApprovalCoordinator.runWithContext`)。 */
|
|
110
|
+
export function runWithGovernanceAskScope(scope, fn) {
|
|
111
|
+
return scopeAls.run(scope, fn);
|
|
112
|
+
}
|
|
113
|
+
/** **写侧**入口:当前异步作用域那一格的视图;作用域外 = `undefined`(写 no-op,见顶注)。 */
|
|
114
|
+
export function currentGovernanceAskMarks() {
|
|
115
|
+
const scope = scopeAls.getStore();
|
|
116
|
+
return scope === undefined ? undefined : createScopedView(processMarks, scope, DEFAULT_MAX_MARKS);
|
|
117
|
+
}
|
|
118
|
+
/** **读侧**入口:按**显式** scope 取视图(读侧不读 ALS —— 理由见顶注 round5 那段)。 */
|
|
119
|
+
export function governanceAskMarksFor(scope) {
|
|
120
|
+
return createScopedView(processMarks, scope, DEFAULT_MAX_MARKS);
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=governance-ask-marks.js.map
|
|
@@ -14,6 +14,21 @@ export declare const MAX_HOOK_MATCHER_GROUPS = 16;
|
|
|
14
14
|
export declare const MAX_HOOK_HTTP_HEADERS = 32;
|
|
15
15
|
/** hook 进程 stdout/stderr 各自的采集上限(字节)——防输出洪泛打爆内存/日志。 */
|
|
16
16
|
export declare const MAX_HOOK_OUTPUT_BYTES: number;
|
|
17
|
+
/**
|
|
18
|
+
* 一次事件里所有 hook 上下文片段**聚合后**喂给模型的总量上限(字符)。
|
|
19
|
+
*
|
|
20
|
+
* 为什么单条帽不够:一次事件最多 {@link MAX_HOOK_ENTRIES_PER_EVENT} 条(parseHooksConfig 的广度闸),
|
|
21
|
+
* 每条最多推两段(`decision:"block"` 的 reason + `hookSpecificOutput.additionalContext`),
|
|
22
|
+
* {@link composeHooks} 再把部署槽与 task 槽的**成品**拼一次 —— 单条 4096 相乘后一次注入可达 ~256KB,
|
|
23
|
+
* 而这些字符是**逐次工具调用**进模型可见文本的(core `hooks.js` 的 preToolContext / `prepare-task.js`
|
|
24
|
+
* 的 tool_result 追加 / `runtask.js` 的 Stop follow-up),不是日志。
|
|
25
|
+
*
|
|
26
|
+
* 帽值取 10_000 的依据:模型可见文本在本栈的既有预算量纲就是 10k —— core `truncateError`
|
|
27
|
+
* (`src/core/tool-errors.ts`)的 10k 中截,逐字锚 CC 序列化器 `A7e` 的 `1e4`;#181 战役里 core 还要给
|
|
28
|
+
* `beforeToolCall` block reason 补一道同值兜底闸。取同一量级 = 「一次 hook 注入 ≤ 一份错误文本预算」,
|
|
29
|
+
* 且 4096 的单条帽保证**前两条**钩子的话仍整段进得去(截断是配置广度的代价,不是常态)。
|
|
30
|
+
*/
|
|
31
|
+
export declare const MAX_HOOK_CONTEXT_TOTAL_CHARS = 10000;
|
|
17
32
|
export interface ParseHooksConfigResult {
|
|
18
33
|
config?: HooksConfig;
|
|
19
34
|
/** 人可读的第一条校验错误(HTTP 层用它 400 fail-loud;resume 防御路径用它 warn)。 */
|
|
@@ -96,6 +111,19 @@ export type HookLlmCall = (opts: {
|
|
|
96
111
|
/** 失败判别码(B8:判别一律走码,禁按 error 文案分支——v3.1 批2,统检第二波 high)。error 仍是给人看的
|
|
97
112
|
* 自由文本;code 是给控制流的。缺席=未分类失败(不重试、不特判)。 */
|
|
98
113
|
export type HookLlmFailureCode = "no_content" | "hard_timeout";
|
|
114
|
+
/**
|
|
115
|
+
* hook 上下文片段 → 喂给模型的单串。**所有** `additionalContext` 聚合点的唯一属主(此前是同一个
|
|
116
|
+
* `contexts.join("\n")` 表达式抄在二十余处,总量帽无处可挂 —— 那正是漂移成因)。
|
|
117
|
+
*
|
|
118
|
+
* 三条语义,都是承重的:
|
|
119
|
+
* 1. **顺序保留、只截尾**:先来的钩子先说话;一旦装不下就停,后面的条目一律不再挤进来(哪怕更短)。
|
|
120
|
+
* 按长度重排会让"第 3 条钩子的话"随别人的长度忽隐忽现,排障时无从复现。
|
|
121
|
+
* 2. **截断可见**:尾巴挂一行标记说明省了几条、丢了多少字符。静默丢弃会让 hook 作者以为自己的
|
|
122
|
+
* context 生效了,而模型那头根本没见过 —— 这类"以为配好了"的缺席比长文本本身更贵。
|
|
123
|
+
* 3. **标记不计预算**:与 core `truncateError` / CC `A7e` 同姿势(marker 不占那 10k),否则帽值的
|
|
124
|
+
* 含义会随标记文案长度漂。
|
|
125
|
+
*/
|
|
126
|
+
export declare function buildHookContext(parts: readonly string[]): string | undefined;
|
|
99
127
|
/**
|
|
100
128
|
* 阶段三a:`http` 条目——契约语义 = POST hook 输入 JSON 到 `url`;headers 里的 `$NAME` 仅当 NAME 列在
|
|
101
129
|
* `allowedEnvVars` 才从 worker 进程 env 插值(配置本身绝不携带密钥值,契约同边界)。
|
|
@@ -65,6 +65,24 @@ export const MAX_HOOK_HTTP_HEADERS = 32;
|
|
|
65
65
|
export const MAX_HOOK_OUTPUT_BYTES = 1024 * 1024;
|
|
66
66
|
/** 喂给模型的 deny 原因/additionalContext 的单条长度上限(字符)。 */
|
|
67
67
|
const MAX_HOOK_FEEDBACK_CHARS = 4_096;
|
|
68
|
+
/**
|
|
69
|
+
* 一次事件里所有 hook 上下文片段**聚合后**喂给模型的总量上限(字符)。
|
|
70
|
+
*
|
|
71
|
+
* 为什么单条帽不够:一次事件最多 {@link MAX_HOOK_ENTRIES_PER_EVENT} 条(parseHooksConfig 的广度闸),
|
|
72
|
+
* 每条最多推两段(`decision:"block"` 的 reason + `hookSpecificOutput.additionalContext`),
|
|
73
|
+
* {@link composeHooks} 再把部署槽与 task 槽的**成品**拼一次 —— 单条 4096 相乘后一次注入可达 ~256KB,
|
|
74
|
+
* 而这些字符是**逐次工具调用**进模型可见文本的(core `hooks.js` 的 preToolContext / `prepare-task.js`
|
|
75
|
+
* 的 tool_result 追加 / `runtask.js` 的 Stop follow-up),不是日志。
|
|
76
|
+
*
|
|
77
|
+
* 帽值取 10_000 的依据:模型可见文本在本栈的既有预算量纲就是 10k —— core `truncateError`
|
|
78
|
+
* (`src/core/tool-errors.ts`)的 10k 中截,逐字锚 CC 序列化器 `A7e` 的 `1e4`;#181 战役里 core 还要给
|
|
79
|
+
* `beforeToolCall` block reason 补一道同值兜底闸。取同一量级 = 「一次 hook 注入 ≤ 一份错误文本预算」,
|
|
80
|
+
* 且 4096 的单条帽保证**前两条**钩子的话仍整段进得去(截断是配置广度的代价,不是常态)。
|
|
81
|
+
*/
|
|
82
|
+
export const MAX_HOOK_CONTEXT_TOTAL_CHARS = 10_000;
|
|
83
|
+
/** 帽满时仍值得保留的最短残段(字符)。比这更短的尾巴不是信息:模型会把半句话读成完整指令,
|
|
84
|
+
* 宁可整条落进"省略"计数。 */
|
|
85
|
+
const MIN_HOOK_CONTEXT_PARTIAL_CHARS = 256;
|
|
68
86
|
/** PostToolUse payload 里 tool_response 文本投影的上限(字符)——工具结果可能是 MB 级。 */
|
|
69
87
|
const MAX_TOOL_RESPONSE_CHARS = 32_768;
|
|
70
88
|
/** stdin payload 里 tool_input JSON 投影的上限(字符;巨大 tool_input 经 JSON.stringify 造成
|
|
@@ -233,6 +251,104 @@ const DEFAULT_AGENT_HOOK_TIMEOUT_SECONDS = 120;
|
|
|
233
251
|
function clip(s, n) {
|
|
234
252
|
return s.length > n ? `${s.slice(0, n)}…(${s.length})` : s;
|
|
235
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* hook 上下文片段 → 喂给模型的单串。**所有** `additionalContext` 聚合点的唯一属主(此前是同一个
|
|
256
|
+
* `contexts.join("\n")` 表达式抄在二十余处,总量帽无处可挂 —— 那正是漂移成因)。
|
|
257
|
+
*
|
|
258
|
+
* 三条语义,都是承重的:
|
|
259
|
+
* 1. **顺序保留、只截尾**:先来的钩子先说话;一旦装不下就停,后面的条目一律不再挤进来(哪怕更短)。
|
|
260
|
+
* 按长度重排会让"第 3 条钩子的话"随别人的长度忽隐忽现,排障时无从复现。
|
|
261
|
+
* 2. **截断可见**:尾巴挂一行标记说明省了几条、丢了多少字符。静默丢弃会让 hook 作者以为自己的
|
|
262
|
+
* context 生效了,而模型那头根本没见过 —— 这类"以为配好了"的缺席比长文本本身更贵。
|
|
263
|
+
* 3. **标记不计预算**:与 core `truncateError` / CC `A7e` 同姿势(marker 不占那 10k),否则帽值的
|
|
264
|
+
* 含义会随标记文案长度漂。
|
|
265
|
+
*/
|
|
266
|
+
export function buildHookContext(parts) {
|
|
267
|
+
const present = parts.filter((p) => p.length > 0).map(readHookContextPart);
|
|
268
|
+
if (present.length === 0)
|
|
269
|
+
return undefined;
|
|
270
|
+
const kept = [];
|
|
271
|
+
let used = 0;
|
|
272
|
+
let truncating = false;
|
|
273
|
+
let messages = 0;
|
|
274
|
+
let omitted = 0;
|
|
275
|
+
let droppedChars = 0;
|
|
276
|
+
for (const part of present) {
|
|
277
|
+
// 内层的账先并进来(标记读回,见 {@link readHookContextPart}),再谈这一轮自己丢了什么。
|
|
278
|
+
messages += part.messages;
|
|
279
|
+
omitted += part.omitted;
|
|
280
|
+
droppedChars += part.dropped;
|
|
281
|
+
if (!truncating) {
|
|
282
|
+
const sep = kept.length > 0 ? 1 : 0; // join 的 "\n" 也占预算
|
|
283
|
+
const room = MAX_HOOK_CONTEXT_TOTAL_CHARS - used - sep;
|
|
284
|
+
if (room >= part.body.length) {
|
|
285
|
+
if (part.body.length > 0) {
|
|
286
|
+
kept.push(part.body);
|
|
287
|
+
used += sep + part.body.length;
|
|
288
|
+
}
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
truncating = true;
|
|
292
|
+
// 只有**单条**发言可以切尾:切了它仍是"部分在场",账目诚实(dropped 记尾巴,omitted 不变)。
|
|
293
|
+
// 已聚合的成品代表内层多条、正文里却没有条目边界 —— 切一刀会把第 2..N 条整个抹掉却记 0 条省略,
|
|
294
|
+
// 标记恰好在最常走的两层合流路上说谎(codex 复审 R4,亲核属实)。⇒ 它要么整段留,要么整条丢。
|
|
295
|
+
if (part.messages === 1 && room >= MIN_HOOK_CONTEXT_PARTIAL_CHARS) {
|
|
296
|
+
kept.push(part.body.slice(0, room));
|
|
297
|
+
used += sep + room;
|
|
298
|
+
droppedChars += part.body.length - room;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// 整条不见 = 它代表的**全部**条目都不见。内层已经报过的那几条上面加过了,这里只补差额
|
|
303
|
+
// ——否则一个"3 of 5"的成品被整条丢掉会记成 8 条,报出"省掉的比总数还多"这种自相矛盾的账。
|
|
304
|
+
omitted += part.messages - part.omitted;
|
|
305
|
+
droppedChars += part.body.length;
|
|
306
|
+
}
|
|
307
|
+
const body = kept.join("\n");
|
|
308
|
+
if (droppedChars === 0)
|
|
309
|
+
return body;
|
|
310
|
+
const marker = `[hook context truncated at ${MAX_HOOK_CONTEXT_TOTAL_CHARS} chars: ${omitted} of ${messages} hook messages omitted, ${droppedChars} characters dropped]`;
|
|
311
|
+
return kept.length > 0 ? `${body}\n${marker}` : marker;
|
|
312
|
+
}
|
|
313
|
+
/** 截断标记的尾锚形。帽值**逐字写进模式**(不是 `\d+`):只认"我们自己这一版才会写出的那一份",
|
|
314
|
+
* 别处飘来的同类文字连模式都不匹配。 */
|
|
315
|
+
const HOOK_CONTEXT_MARKER_RE = new RegExp(`\\n\\[hook context truncated at ${MAX_HOOK_CONTEXT_TOTAL_CHARS} chars: (\\d+) of (\\d+) hook messages omitted, (\\d+) characters dropped\\]$`);
|
|
316
|
+
/** 读回标记时接受的数字上界(2^42)。比任何真值大若干数量级,又小到几十条相加也绝不越过安全整数
|
|
317
|
+
* —— 伪造的巨数因此根本进不来,合并加法也不必每一步复验溢出。 */
|
|
318
|
+
const MAX_HOOK_CONTEXT_PARSED_COUNT = 2 ** 42;
|
|
319
|
+
/**
|
|
320
|
+
* 片段 → 内容 + 它自带的截断账。
|
|
321
|
+
*
|
|
322
|
+
* 为什么必须能**读回**自己写的标记:`composeHooks` 会把 {@link buildHookContext} 的成品当片段再喂一次
|
|
323
|
+
* (部署槽在场即走这条路 —— 是常态不是边角)。标记按内容处理会犯两桩:①切在标记中间 ⇒ 模型读到半截
|
|
324
|
+
* 标记;②外层重新记账 ⇒ 内层丢掉的那一万多字符凭空消失,标记数字变成谎。标记既然不计预算,读回时也
|
|
325
|
+
* 不能算内容;把它携带的账并进外层,函数对自己的输出就是幂等的。
|
|
326
|
+
*
|
|
327
|
+
* 🔴 这把尺子对着的是**不可信文本**(钩子想吐什么就吐什么),所以只认"我们自己才写得出的那一份":
|
|
328
|
+
* 帽值逐字相等(在模式里)、`dropped ≥ 1`(我们从不在零丢弃时写标记)、`omitted ≤ messages`、
|
|
329
|
+
* `messages ≥ 1`、数字都在 {@link MAX_HOOK_CONTEXT_PARSED_COUNT} 内。任一条不合 ⇒ **整段当普通内容**。
|
|
330
|
+
* 认下之后也不会有"内容静默少一行":认下 ⇒ `dropped > 0` ⇒ 输出必然带标记 —— 摘掉的那一行总是被
|
|
331
|
+
* **重写**回去(数字可能被合并),从不是被删掉。伪造得刚好合规只会让记账口径偏,不改任何裁决;
|
|
332
|
+
* 而且钩子配置在单用户闸内,伪造者就是这份账的读者本人。
|
|
333
|
+
*/
|
|
334
|
+
function readHookContextPart(part) {
|
|
335
|
+
const m = HOOK_CONTEXT_MARKER_RE.exec(part);
|
|
336
|
+
if (!m)
|
|
337
|
+
return { body: part, messages: 1, omitted: 0, dropped: 0 };
|
|
338
|
+
const omitted = Number(m[1]);
|
|
339
|
+
const messages = Number(m[2]);
|
|
340
|
+
const dropped = Number(m[3]);
|
|
341
|
+
const sane = (n) => Number.isSafeInteger(n) && n <= MAX_HOOK_CONTEXT_PARSED_COUNT;
|
|
342
|
+
if (!sane(omitted) || !sane(messages) || !sane(dropped) || messages < 1 || omitted > messages || dropped < 1) {
|
|
343
|
+
return { body: part, messages: 1, omitted: 0, dropped: 0 };
|
|
344
|
+
}
|
|
345
|
+
return { body: part.slice(0, part.length - m[0].length), messages, omitted, dropped };
|
|
346
|
+
}
|
|
347
|
+
/** {@link buildHookContext} 的字段形(聚合点全是 `...spread` 进结果对象):空 → 不产字段。 */
|
|
348
|
+
function buildHookContextField(parts) {
|
|
349
|
+
const text = buildHookContext(parts);
|
|
350
|
+
return text === undefined ? {} : { additionalContext: text };
|
|
351
|
+
}
|
|
236
352
|
/** 跑一条 command hook:sh -c,stdin=payload JSON(CC-verbatim),超时 SIGKILL,输出各限 1MiB。 */
|
|
237
353
|
function runCommandHook(entry, payload, ctx) {
|
|
238
354
|
return new Promise((resolve) => {
|
|
@@ -736,7 +852,7 @@ export function createTaskHooks(config, ctx) {
|
|
|
736
852
|
if (run.code === 2) {
|
|
737
853
|
// CC:exit 2 = 阻断,stderr 喂给模型
|
|
738
854
|
const reason = clip(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS) || "blocked by a PreToolUse hook";
|
|
739
|
-
return { action: "deny", message: reason, ...(contexts
|
|
855
|
+
return { action: "deny", message: reason, ...buildHookContextField(contexts) };
|
|
740
856
|
}
|
|
741
857
|
if (run.code !== 0) {
|
|
742
858
|
ctx.logger.warn("hook_command_failed", { event: "PreToolUse", code: run.code, stderr: clip(run.stderr, 300) });
|
|
@@ -750,11 +866,11 @@ export function createTaskHooks(config, ctx) {
|
|
|
750
866
|
if (out.continue === false) {
|
|
751
867
|
const reason = clip(typeof out.stopReason === "string" ? out.stopReason : "hook requested stop", MAX_HOOK_FEEDBACK_CHARS);
|
|
752
868
|
// core 的 preToolUse seam 无"终止整个 run"能力(阶段一诚实近似:deny 本次调用)。
|
|
753
|
-
return { action: "deny", message: reason, ...(contexts
|
|
869
|
+
return { action: "deny", message: reason, ...buildHookContextField(contexts) };
|
|
754
870
|
}
|
|
755
871
|
if (out.decision === "block") {
|
|
756
872
|
const reason = clip(typeof out.reason === "string" ? out.reason : "blocked by a PreToolUse hook", MAX_HOOK_FEEDBACK_CHARS);
|
|
757
|
-
return { action: "deny", message: reason, ...(contexts
|
|
873
|
+
return { action: "deny", message: reason, ...buildHookContextField(contexts) };
|
|
758
874
|
}
|
|
759
875
|
const hso = out.hookSpecificOutput;
|
|
760
876
|
if (hso !== null && typeof hso === "object" && hso.hookEventName === "PreToolUse") {
|
|
@@ -764,7 +880,7 @@ export function createTaskHooks(config, ctx) {
|
|
|
764
880
|
const decision = h.permissionDecision;
|
|
765
881
|
if (decision === "deny") {
|
|
766
882
|
const reason = clip(typeof h.permissionDecisionReason === "string" ? h.permissionDecisionReason : "denied by a PreToolUse hook", MAX_HOOK_FEEDBACK_CHARS);
|
|
767
|
-
return { action: "deny", message: reason, ...(contexts
|
|
883
|
+
return { action: "deny", message: reason, ...buildHookContextField(contexts) };
|
|
768
884
|
}
|
|
769
885
|
if (decision === "ask") {
|
|
770
886
|
asked = clip(typeof h.permissionDecisionReason === "string" ? h.permissionDecisionReason : "a PreToolUse hook requires approval", MAX_HOOK_FEEDBACK_CHARS);
|
|
@@ -798,7 +914,7 @@ export function createTaskHooks(config, ctx) {
|
|
|
798
914
|
action: "ask",
|
|
799
915
|
message: asked,
|
|
800
916
|
...(rewritten ? { updatedInput: currentInput } : {}),
|
|
801
|
-
...(contexts
|
|
917
|
+
...buildHookContextField(contexts),
|
|
802
918
|
};
|
|
803
919
|
}
|
|
804
920
|
if (unknownDecision) {
|
|
@@ -814,14 +930,14 @@ export function createTaskHooks(config, ctx) {
|
|
|
814
930
|
action: "ask",
|
|
815
931
|
message: "a PreToolUse hook returned an unrecognized permissionDecision — approval required",
|
|
816
932
|
...(rewritten ? { updatedInput: currentInput } : {}),
|
|
817
|
-
...(contexts
|
|
933
|
+
...buildHookContextField(contexts),
|
|
818
934
|
};
|
|
819
935
|
}
|
|
820
936
|
if (rewritten || contexts.length > 0) {
|
|
821
937
|
return {
|
|
822
938
|
action: "allow",
|
|
823
939
|
...(rewritten ? { updatedInput: currentInput } : {}),
|
|
824
|
-
...(contexts
|
|
940
|
+
...buildHookContextField(contexts),
|
|
825
941
|
};
|
|
826
942
|
}
|
|
827
943
|
return undefined; // 无意见(≠ allow 短路;core 门照常裁决)
|
|
@@ -879,7 +995,8 @@ export function createTaskHooks(config, ctx) {
|
|
|
879
995
|
}
|
|
880
996
|
}
|
|
881
997
|
}
|
|
882
|
-
|
|
998
|
+
const additionalContext = buildHookContext(contexts);
|
|
999
|
+
return additionalContext === undefined ? undefined : { additionalContext };
|
|
883
1000
|
};
|
|
884
1001
|
hooks.postToolUse = postToolUseHook;
|
|
885
1002
|
}
|
|
@@ -927,7 +1044,8 @@ export function createTaskHooks(config, ctx) {
|
|
|
927
1044
|
contexts.push(clip(h.additionalContext, MAX_HOOK_FEEDBACK_CHARS));
|
|
928
1045
|
}
|
|
929
1046
|
}
|
|
930
|
-
|
|
1047
|
+
const additionalContext = buildHookContext(contexts);
|
|
1048
|
+
return additionalContext === undefined ? undefined : { additionalContext };
|
|
931
1049
|
};
|
|
932
1050
|
hooks.postToolUseFailure = postToolUseFailureHook;
|
|
933
1051
|
}
|
|
@@ -973,7 +1091,8 @@ export function createTaskHooks(config, ctx) {
|
|
|
973
1091
|
if (out.hookSpecificOutput !== undefined)
|
|
974
1092
|
ctx.logger.warn("hook_result_unsupported", { event: "PostToolBatch" });
|
|
975
1093
|
}
|
|
976
|
-
|
|
1094
|
+
const additionalContext = buildHookContext(contexts);
|
|
1095
|
+
return additionalContext === undefined ? undefined : { additionalContext };
|
|
977
1096
|
};
|
|
978
1097
|
hooks.postToolBatch = postToolBatchHook;
|
|
979
1098
|
}
|
|
@@ -989,7 +1108,7 @@ export function createTaskHooks(config, ctx) {
|
|
|
989
1108
|
for (const { run } of singles) {
|
|
990
1109
|
if (run.code === 2) {
|
|
991
1110
|
const reason = clip(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS) || "blocked by a UserPromptSubmit hook";
|
|
992
|
-
return { block: reason, ...(contexts
|
|
1111
|
+
return { block: reason, ...buildHookContextField(contexts) };
|
|
993
1112
|
}
|
|
994
1113
|
if (run.code !== 0) {
|
|
995
1114
|
ctx.logger.warn("hook_command_failed", { event: "UserPromptSubmit", code: run.code, stderr: clip(run.stderr, 300) });
|
|
@@ -1003,11 +1122,11 @@ export function createTaskHooks(config, ctx) {
|
|
|
1003
1122
|
if (out.continue === false) {
|
|
1004
1123
|
// continue:false=终止整个 run;userPromptSubmit 的 block 语义正是"任务带理由收场"——同义直映射。
|
|
1005
1124
|
const reason = clip(typeof out.stopReason === "string" ? out.stopReason : "hook requested stop", MAX_HOOK_FEEDBACK_CHARS);
|
|
1006
|
-
return { block: reason, ...(contexts
|
|
1125
|
+
return { block: reason, ...buildHookContextField(contexts) };
|
|
1007
1126
|
}
|
|
1008
1127
|
if (out.decision === "block") {
|
|
1009
1128
|
const reason = clip(typeof out.reason === "string" ? out.reason : "blocked by a UserPromptSubmit hook", MAX_HOOK_FEEDBACK_CHARS);
|
|
1010
|
-
return { block: reason, ...(contexts
|
|
1129
|
+
return { block: reason, ...buildHookContextField(contexts) };
|
|
1011
1130
|
}
|
|
1012
1131
|
const hso = out.hookSpecificOutput;
|
|
1013
1132
|
if (hso !== null && typeof hso === "object" && hso.hookEventName === "UserPromptSubmit") {
|
|
@@ -1016,7 +1135,8 @@ export function createTaskHooks(config, ctx) {
|
|
|
1016
1135
|
contexts.push(clip(h.additionalContext, MAX_HOOK_FEEDBACK_CHARS));
|
|
1017
1136
|
}
|
|
1018
1137
|
}
|
|
1019
|
-
|
|
1138
|
+
const additionalContext = buildHookContext(contexts);
|
|
1139
|
+
return additionalContext === undefined ? undefined : { additionalContext };
|
|
1020
1140
|
};
|
|
1021
1141
|
hooks.userPromptSubmit = userPromptSubmitHook;
|
|
1022
1142
|
}
|
|
@@ -1180,7 +1300,7 @@ export function createTaskHooks(config, ctx) {
|
|
|
1180
1300
|
return undefined;
|
|
1181
1301
|
return {
|
|
1182
1302
|
...(block !== undefined ? { block } : {}),
|
|
1183
|
-
...(contexts
|
|
1303
|
+
...buildHookContextField(contexts),
|
|
1184
1304
|
};
|
|
1185
1305
|
};
|
|
1186
1306
|
hooks.stop = stopHook;
|
|
@@ -1288,7 +1408,7 @@ export function composeHooks(deployment, task) {
|
|
|
1288
1408
|
if (r.additionalContext)
|
|
1289
1409
|
contexts.push(r.additionalContext);
|
|
1290
1410
|
if (r.action === "deny")
|
|
1291
|
-
return { ...r, ...(contexts
|
|
1411
|
+
return { ...r, ...buildHookContextField(contexts) };
|
|
1292
1412
|
if (r.action === "ask")
|
|
1293
1413
|
asked = r;
|
|
1294
1414
|
if (r.action === "allow" && r.updatedInput !== undefined) {
|
|
@@ -1301,14 +1421,14 @@ export function composeHooks(deployment, task) {
|
|
|
1301
1421
|
return {
|
|
1302
1422
|
...asked,
|
|
1303
1423
|
...(askedUpdated === undefined && current !== input ? { updatedInput: current } : {}),
|
|
1304
|
-
...(contexts
|
|
1424
|
+
...buildHookContextField(contexts),
|
|
1305
1425
|
};
|
|
1306
1426
|
}
|
|
1307
1427
|
if (current !== input || contexts.length > 0) {
|
|
1308
1428
|
return {
|
|
1309
1429
|
action: "allow",
|
|
1310
1430
|
...(current !== input ? { updatedInput: current } : {}),
|
|
1311
|
-
...(contexts
|
|
1431
|
+
...buildHookContextField(contexts),
|
|
1312
1432
|
};
|
|
1313
1433
|
}
|
|
1314
1434
|
return undefined;
|
|
@@ -1324,7 +1444,7 @@ export function composeHooks(deployment, task) {
|
|
|
1324
1444
|
const updatedOutput = rt?.updatedOutput ?? rd?.updatedOutput;
|
|
1325
1445
|
return {
|
|
1326
1446
|
...(updatedOutput ? { updatedOutput } : {}),
|
|
1327
|
-
...(contexts
|
|
1447
|
+
...buildHookContextField(contexts),
|
|
1328
1448
|
};
|
|
1329
1449
|
};
|
|
1330
1450
|
}
|
|
@@ -1333,7 +1453,8 @@ export function composeHooks(deployment, task) {
|
|
|
1333
1453
|
const rd = d.postToolUseFailure ? await d.postToolUseFailure(toolName, input, failure, ctx) : undefined;
|
|
1334
1454
|
const rt = t.postToolUseFailure ? await t.postToolUseFailure(toolName, input, failure, ctx) : undefined;
|
|
1335
1455
|
const contexts = [rd?.additionalContext, rt?.additionalContext].filter((c) => typeof c === "string" && c.length > 0);
|
|
1336
|
-
|
|
1456
|
+
const additionalContext = buildHookContext(contexts);
|
|
1457
|
+
return additionalContext === undefined ? undefined : { additionalContext };
|
|
1337
1458
|
};
|
|
1338
1459
|
}
|
|
1339
1460
|
if (d.postToolBatch || t.postToolBatch) {
|
|
@@ -1341,19 +1462,22 @@ export function composeHooks(deployment, task) {
|
|
|
1341
1462
|
const rd = d.postToolBatch ? await d.postToolBatch(calls) : undefined;
|
|
1342
1463
|
const rt = t.postToolBatch ? await t.postToolBatch(calls) : undefined;
|
|
1343
1464
|
const contexts = [rd?.additionalContext, rt?.additionalContext].filter((c) => typeof c === "string" && c.length > 0);
|
|
1344
|
-
|
|
1465
|
+
const additionalContext = buildHookContext(contexts);
|
|
1466
|
+
return additionalContext === undefined ? undefined : { additionalContext };
|
|
1345
1467
|
};
|
|
1346
1468
|
}
|
|
1347
1469
|
if (d.userPromptSubmit || t.userPromptSubmit) {
|
|
1348
1470
|
out.userPromptSubmit = async (prompt) => {
|
|
1349
1471
|
const rd = d.userPromptSubmit ? await d.userPromptSubmit(prompt) : undefined;
|
|
1472
|
+
// 短路的是**槽序**(部署拦下就不再跑 task 槽),不是预算:`additionalContext` 与 `block` 在 core
|
|
1473
|
+
// 是解耦两字段(见下方 stop 注),原样外送就是一条绕过总帽的道。
|
|
1350
1474
|
if (rd?.block)
|
|
1351
|
-
return rd;
|
|
1475
|
+
return { ...rd, ...buildHookContextField(rd.additionalContext ? [rd.additionalContext] : []) };
|
|
1352
1476
|
const rt = t.userPromptSubmit ? await t.userPromptSubmit(prompt) : undefined;
|
|
1353
1477
|
if (!rd && !rt)
|
|
1354
1478
|
return undefined;
|
|
1355
1479
|
const contexts = [rd?.additionalContext, rt?.additionalContext].filter((c) => typeof c === "string" && c.length > 0);
|
|
1356
|
-
return { ...(rt?.block ? { block: rt.block } : {}), ...(contexts
|
|
1480
|
+
return { ...(rt?.block ? { block: rt.block } : {}), ...buildHookContextField(contexts) };
|
|
1357
1481
|
};
|
|
1358
1482
|
}
|
|
1359
1483
|
if (d.stop || t.stop) {
|
|
@@ -1362,12 +1486,12 @@ export function composeHooks(deployment, task) {
|
|
|
1362
1486
|
out.stop = async (sctx) => {
|
|
1363
1487
|
const rd = d.stop ? await d.stop(sctx) : undefined;
|
|
1364
1488
|
if (rd?.block)
|
|
1365
|
-
return rd;
|
|
1489
|
+
return { ...rd, ...buildHookContextField(rd.additionalContext ? [rd.additionalContext] : []) };
|
|
1366
1490
|
const rt = t.stop ? await t.stop(sctx) : undefined;
|
|
1367
1491
|
if (!rd && !rt)
|
|
1368
1492
|
return undefined;
|
|
1369
1493
|
const contexts = [rd?.additionalContext, rt?.additionalContext].filter((c) => typeof c === "string" && c.length > 0);
|
|
1370
|
-
return { ...(rt?.block ? { block: rt.block } : {}), ...(contexts
|
|
1494
|
+
return { ...(rt?.block ? { block: rt.block } : {}), ...buildHookContextField(contexts) };
|
|
1371
1495
|
};
|
|
1372
1496
|
}
|
|
1373
1497
|
if (d.preCompact || t.preCompact) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { principalFrom, decodeCheckpointScope } from "../../security.js";
|
|
1
|
+
import { principalFrom, decodeCheckpointScope, PRINCIPAL_TOKEN_HEADER, APPROVAL_MAC_HEADER, APPROVAL_MAC_KID_HEADER } from "../../security.js";
|
|
2
2
|
import { verifyDirectDoorProof } from "../../principal-jwt.js";
|
|
3
3
|
import { MAX_APPROVAL_REASON_CHARS } from "../../approval-hmac.js";
|
|
4
4
|
import { redactedPreview } from "../../trace/redact.js";
|
|
5
5
|
import { fleetRunLabels } from "../../fleet/fleet-bus.js"; // [2069]④ §3 行展示名与 fleet 行同源(见用处的注)
|
|
6
6
|
import { sleep } from "../sse-log.js";
|
|
7
|
-
import { sendJson, sendError, sseHeaders } from "../send.js";
|
|
7
|
+
import { sendJson, sendError, sseHeaders, SSE_MAX_STREAM_MS, SSE_HEARTBEAT_IDLE_MS } from "../send.js";
|
|
8
8
|
import { gatedPrincipal, explicitOperatorOk, isOperator } from "../principal-gate.js";
|
|
9
9
|
// design/80 seam #2 (assistant-scheduler): graceful preempt (durable yield) + resource_limit resume of one task.
|
|
10
10
|
export const ASSISTANT_PREEMPT_RE = /^\/v1\/assistant\/tasks\/([^/]+)\/preempt$/;
|
|
@@ -427,7 +427,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
427
427
|
let deciderPrincipal = principal;
|
|
428
428
|
if (deps.config.directDoorActive) {
|
|
429
429
|
const hdr = (n) => { const h = req.headers[n]; return Array.isArray(h) ? h[0] : h; };
|
|
430
|
-
const proof = verifyDirectDoorProof({ jwt: hdr(
|
|
430
|
+
const proof = verifyDirectDoorProof({ jwt: hdr(PRINCIPAL_TOKEN_HEADER), mac: hdr(APPROVAL_MAC_HEADER), kid: hdr(APPROVAL_MAC_KID_HEADER) },
|
|
431
431
|
// reason 入签:同一条规则也适用于身份绑定腿(两个绑定位为 null,第五位照常参与)。
|
|
432
432
|
{ sessionId: run.sessionId, decision, reason: typeof body.reason === "string" ? body.reason : null }, deps.config, { actionBinding: false });
|
|
433
433
|
if (!proof.ok) {
|
|
@@ -569,7 +569,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
569
569
|
const h = req.headers[n];
|
|
570
570
|
return Array.isArray(h) ? h[0] : h;
|
|
571
571
|
};
|
|
572
|
-
const proof = verifyDirectDoorProof({ jwt: hdr(
|
|
572
|
+
const proof = verifyDirectDoorProof({ jwt: hdr(PRINCIPAL_TOKEN_HEADER), mac: hdr(APPROVAL_MAC_HEADER), kid: hdr(APPROVAL_MAC_KID_HEADER) },
|
|
573
573
|
// reason 入签(三方裁 (a) + clay 干净切):落账理由属于决定内容,与 decision 同受完整性保护。
|
|
574
574
|
{ sessionId, boundCallId: binding.boundCallId, boundInputHash: binding.boundInputHash, decision, reason: typeof body.reason === "string" ? body.reason : null }, deps.config);
|
|
575
575
|
if (!proof.ok) {
|
|
@@ -682,7 +682,6 @@ export async function streamApprovals(req, res, cs, scope, pollMs = APPROVALS_ST
|
|
|
682
682
|
let closed = false;
|
|
683
683
|
req.on("close", () => { closed = true; });
|
|
684
684
|
const start = Date.now();
|
|
685
|
-
const MAX_MS = 15 * 60 * 1000;
|
|
686
685
|
let lastBeat = Date.now();
|
|
687
686
|
let prev = new Map();
|
|
688
687
|
let first = true;
|
|
@@ -715,11 +714,11 @@ export async function streamApprovals(req, res, cs, scope, pollMs = APPROVALS_ST
|
|
|
715
714
|
res.write(`event: resolved\ndata: ${JSON.stringify({ type: "resolved", sessionId: p.sessionId, toolCallId: p.toolCallId })}\n\n`); // decided/expired/gone
|
|
716
715
|
}
|
|
717
716
|
prev = cur;
|
|
718
|
-
if (Date.now() - start >
|
|
717
|
+
if (Date.now() - start > SSE_MAX_STREAM_MS) {
|
|
719
718
|
res.write(`event: error\ndata: ${JSON.stringify({ type: "error", errorCode: "STREAM_MAX_DURATION", message: "approvals stream reached its 15-minute cap — reconnect to continue" })}\n\n`);
|
|
720
719
|
break;
|
|
721
720
|
}
|
|
722
|
-
if (Date.now() - lastBeat >
|
|
721
|
+
if (Date.now() - lastBeat > SSE_HEARTBEAT_IDLE_MS) {
|
|
723
722
|
res.write(`event: heartbeat\ndata: ${JSON.stringify({ type: "heartbeat" })}\n\n`);
|
|
724
723
|
lastBeat = Date.now();
|
|
725
724
|
}
|
|
@@ -58,14 +58,19 @@ export function buildStaticWiringAudit(facts) {
|
|
|
58
58
|
// park 车道的**持久性读数**(codex 交叉复审 2026-08-07 R2-F2,验真后部分采纳)。`capable` 说的是
|
|
59
59
|
// 「有没有 checkpoint 店」、`effective` 说的是「这条部署形会不会真 park」—— 两者都为真时仍可能是一个
|
|
60
60
|
// 重启即丢的店,那样窗到期后被 park 的审批在重启后无法赎回,而流内审批协议正是拿 park 当降级目的地。
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
61
|
+
// ✅ **读数已转可信**(#168 件5,2026-08-07):树上三条 checkpoint 店(SQL 双生 + local 文件形)现在都
|
|
62
|
+
// 如实声明了 `durability`,所以这条读数不再对真持久部署恒假 —— 它现在说的就是字面意思。
|
|
63
|
+
//
|
|
64
|
+
// 🔴 仍然是 **warn 而不是拒启**,判据与 session 那条**刻意不同**,理由写在这里免得下一个人以为是漏改:
|
|
65
|
+
// session 那条对表的是「声明 durable × 介质不 durable」——两个已知量互相矛盾,拒启是查出了谎。这一条
|
|
66
|
+
// 对表的是「有没有人声明」,而 core 把**缺席**折成 `process-local`;一个合法的第三方/新增 checkpoint
|
|
67
|
+
// 店没表态时,拒启会挡住一个本来跑得好好的部署,而**没有任何旋钮**能让它继续启动(补偿为零 ⇒ 不升格)。
|
|
68
|
+
// 升格的前置件 = core 把「未声明」与「声明为进程内」分成两个可区分读数,或本仓给出一个显式承认旋钮。
|
|
64
69
|
if (facts.parkFacility && facts.manifest.parkLane.checkpointDurability !== "declared_durable") {
|
|
65
70
|
warnings.push(`park facility is enabled but the checkpoint store does not declare durable storage ` +
|
|
66
71
|
`(parkLane.checkpointDurability=${JSON.stringify(facts.manifest.parkLane.checkpointDurability ?? null)}). ` +
|
|
67
|
-
`A parked approval survives a restart only on a durable store
|
|
68
|
-
`Treat park-lane recovery as unproven until
|
|
72
|
+
`A parked approval survives a restart only on a durable store, and this reading folds "declared process-local" together with "declared nothing" — every in-tree adapter declares "durable", so a non-durable reading here means a store this build does not own. ` +
|
|
73
|
+
`Treat park-lane recovery as unproven until that adapter declares its durability.`);
|
|
69
74
|
}
|
|
70
75
|
if (facts.manifest.interaction.posture === "interactive" && facts.manifest.question.wired !== "wired") {
|
|
71
76
|
warnings.push(`interactionPosture declares "interactive" but no question delivery face is wired (question.wired=${JSON.stringify(facts.manifest.question.wired)}) — ` +
|