@sema-agent/server 7.3.0 → 7.5.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/USAGE.md +63 -0
- package/dist/approval-card.d.ts +15 -3
- package/dist/approval-card.js +41 -7
- package/dist/approval-reconciler.d.ts +120 -16
- package/dist/approval-reconciler.js +146 -19
- package/dist/boot/coordinators.js +13 -3
- package/dist/boot/deferred-sandbox-path-env.d.ts +99 -0
- package/dist/boot/deferred-sandbox-path-env.js +279 -0
- package/dist/boot/execution-env.js +11 -1
- package/dist/boot/org-memory.d.ts +6 -0
- package/dist/boot/org-memory.js +1 -1
- package/dist/boot/reapers.d.ts +2 -0
- package/dist/boot/reapers.js +11 -4
- package/dist/boot/resolve-spec.d.ts +3 -2
- package/dist/boot/resolve-spec.js +175 -63
- 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/capabilities/center-prompts.js +4 -1
- package/dist/config-center/apply-effective.js +33 -10
- package/dist/config-types.d.ts +32 -9
- package/dist/config.d.ts +6 -1
- package/dist/config.js +65 -12
- package/dist/elicitation.js +5 -1
- package/dist/env-facts.d.ts +3 -1
- package/dist/env-facts.js +3 -1
- package/dist/fleet/fleet-bus.d.ts +6 -1
- package/dist/fleet/fleet-bus.js +25 -3
- 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 +180 -24
- package/dist/http/routes/diagnostics.d.ts +84 -0
- package/dist/http/routes/diagnostics.js +145 -0
- 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 +1 -1
- package/dist/http/routes/tasks.js +87 -29
- package/dist/http/server.d.ts +10 -0
- package/dist/http/server.js +29 -12
- package/dist/http/wire-types.d.ts +7 -2
- package/dist/main.js +51 -8
- package/dist/observability/fail-open.d.ts +109 -0
- package/dist/observability/fail-open.js +227 -0
- package/dist/observability/prompt-manifest.d.ts +17 -0
- package/dist/observability/prompt-manifest.js +8 -0
- package/dist/orchestration/workflow-notify-journal.d.ts +57 -1
- package/dist/orchestration/workflow-notify-journal.js +137 -32
- 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 +27 -5
- package/dist/plugins/approval-ask-store-sql.js +9 -2
- package/dist/plugins/background-shell-support.d.ts +1 -1
- package/dist/plugins/background-shell-support.js +2 -2
- package/dist/plugins/checkpoint-store-sql.d.ts +62 -6
- package/dist/plugins/checkpoint-store-sql.js +71 -11
- 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 +57 -4
- package/dist/question.d.ts +18 -14
- package/dist/question.js +83 -34
- package/dist/runs.d.ts +8 -0
- package/dist/runs.js +15 -2
- package/dist/runtime-governance.d.ts +18 -0
- package/dist/runtime-governance.js +90 -3
- package/dist/task-settings.d.ts +16 -21
- package/dist/task-settings.js +22 -19
- package/dist/tool-approval.d.ts +33 -6
- package/dist/tool-approval.js +95 -30
- package/dist/trace/core-keyset-guard.d.ts +17 -3
- package/dist/trace/project.d.ts +36 -1
- package/dist/trace/project.js +55 -2
- package/package.json +3 -3
- package/dist/boot/lexical-path-env.d.ts +0 -14
- package/dist/boot/lexical-path-env.js +0 -116
package/dist/config-types.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { SealedKeyPoison } from "./sealed-key.js";
|
|
|
9
9
|
import type { McpServerSpec, Model, ModelRoles } from "@sema-agent/core";
|
|
10
10
|
import type { ApprovalHmacKey, PrincipalJwtKey } from "./auth-keys.js";
|
|
11
11
|
import type { ElicitationThrottle } from "./elicitation.js";
|
|
12
|
+
import type { QuestionThrottle } from "./question.js";
|
|
12
13
|
import type { InfraCostRates } from "./observability/cost-taxonomy.js";
|
|
13
14
|
import type { Autonomy, CommandRule } from "./runtime-governance.js";
|
|
14
15
|
/** A sema-registry MCP server resolved to a core spec (env-NAME refs already → real values) plus the
|
|
@@ -24,12 +25,12 @@ export interface ScopedMcpServer {
|
|
|
24
25
|
/** #151 车3(design/172 流内审批协议)的配置面。总开关默认 **false**;四个从属旋钮只在开关为真时生效
|
|
25
26
|
* (语义与「谁需要 / 谁被伤 / 什么补偿」三问见 `ServiceConfigFlat.streamApproval` 的注)。 */
|
|
26
27
|
export interface StreamApprovalConfig {
|
|
27
|
-
/** 协议总开关。`STREAM_APPROVAL_ENABLED`,默认 **false
|
|
28
|
+
/** 协议总开关。`STREAM_APPROVAL_ENABLED`,默认 **true**(clay 裁 2026-08-08,#164 翻真验证后;7.5.0 起)。显式 false ⇒ 全链逐字 7.3.0 前行为(唯一干净还原键)。
|
|
28
29
|
* 翻真要等回决端点(车4)与对账收敛器(车5)到位——在此之前是个不完整协议(有卡无正规回决口、
|
|
29
30
|
* 有 PARKING 无收敛器、跨副本回决无唤醒路径)。 */
|
|
30
31
|
enabled: boolean;
|
|
31
32
|
/** 开关为真时作为 `ToolApprovalCoordinator` 的 `ttlMs`(design/172 §3.3 的「配置窗默认」)。
|
|
32
|
-
* `STREAM_ASK_WINDOW_MS`,默认 **
|
|
33
|
+
* `STREAM_ASK_WINDOW_MS`,默认 **300000**(同裁与 sync 腿既有 5min 活卡窗对齐)。`0` = 运维显式关窗 ⇒ 恒 park(§3.3 窗=0 语义)。
|
|
33
34
|
* 开关为假 ⇒ 本值不参与,窗保持既有 `DEFAULT_APPROVAL_TTL_MS`(5min)。 */
|
|
34
35
|
windowMs: number;
|
|
35
36
|
/** 开流重放(§5)每次最多投几张未决卡——**读面**的帽,超出只投最新的并记一次 warn,开流不失败。
|
|
@@ -748,12 +749,28 @@ export interface ServiceConfigFlat {
|
|
|
748
749
|
* facts only ride when the deployment actually knows them (image binding/pkgSource/egress), absent = block
|
|
749
750
|
* unchanged. `SANDBOX_ENV_FACTS=false` opts out. */
|
|
750
751
|
envFactsEnabled: boolean;
|
|
751
|
-
/** ③ sensitive-path write DENY set (core 1.295 `createSensitivePathPolicy
|
|
752
|
-
*
|
|
753
|
-
*
|
|
754
|
-
*
|
|
755
|
-
*
|
|
756
|
-
*
|
|
752
|
+
/** ③ sensitive-path write DENY set (core 1.295 `createSensitivePathPolicy`). The PATTERN SET is core's call
|
|
753
|
+
* (RECOMMENDED_SENSITIVE_PATTERNS — .env/.ssh/keys/.git hooks+config/cloud creds/histories, rationale
|
|
754
|
+
* documented core-side); the server only passes it through. `SENSITIVE_WRITE_PATTERNS` (comma-separated):
|
|
755
|
+
* explicitly set = FULL REPLACEMENT of the recommended set (not a merge); `off` or an empty value = disabled
|
|
756
|
+
* (empty array); unset = core's recommended set.
|
|
757
|
+
* 🔴 **施加点 = governance 层,无条件(#177 / [2951] issue #29;搬家前「composed into the fs-write gate fold
|
|
758
|
+
* on host-semantics lanes — task-settings.ts」的旧描述已作废)**:与 `AUTONOMY`/`MANUAL_MODE_SHELL_GATE`
|
|
759
|
+
* 同拍,resolve-spec 预铸成一条纯 DENY policy 递给 `applyRuntimeGovernance`,**不看客户端 settings/
|
|
760
|
+
* permissionMode 的在场性、不看落在哪个模式臂、不看执行 lane**。旧家把它唯一的合成点挂在
|
|
761
|
+
* `deriveSettingsPolicy` 的 fs-write gate 闭包里,而那个闭包只在 default/auto/acceptEdits 三臂被调用 ⇒
|
|
762
|
+
* `bypassPermissions` / `permissionMode` 键缺席(headless `-p` 的默认姿势)/ body 连 settings 都没有
|
|
763
|
+
* 这三形整条 DENY 腿不建(邻仓 46 格真机矩阵实证)。裁决 env 仍按 lane 分形(host 真 fs / 沙箱
|
|
764
|
+
* `DeferredSandboxPathEnv` 代理),与 fs-write ask 门同一份、单点构造。
|
|
765
|
+
* 折叠仍是 deny-wins(tightenTaskSpec ⇒ combinePolicies),所以会话豁免 / acceptEdits 自动 allow /
|
|
766
|
+
* 客户端 settings allow 一律越不过它;非守卫目标该 policy 返回 `action:"allow"`(core 的 ToolPolicy
|
|
767
|
+
* 没有「无意见」第三态;这个 allow 在 combinePolicies 的 deny/ask 优先折叠里不构成一票,故在折叠
|
|
768
|
+
* 语境下等价于弃权)⇒ 不给 bypass 加 ask 门。
|
|
769
|
+
* ⚠️ 非法模式(不含任何路径段,如 `"/"`)在 boot 期 fail-loud(createResolveSpec 先编译一次)——
|
|
770
|
+
* **对存量部署是行为变更**:搬家前这种坏值只在 default/auto/acceptEdits 三臂上每请求炸,只跑
|
|
771
|
+
* headless 的部署带着坏值也能起服务;现在起不来(方向=运维当场看见,而不是每任务一条 500)。
|
|
772
|
+
* ⚠️ 沙箱 lane 的相对形写目标有一格今天无人裁决(cwd 里的守卫段看不见)——边界与两条收口路
|
|
773
|
+
* 成文在 boot/resolve-spec.ts 的 `RelativeTargetLexicalEnv` 类注。 */
|
|
757
774
|
sensitiveWritePatterns: string[];
|
|
758
775
|
/** [1557]§四 opt-in (cli[1555]② finding, core[1556] suggested mechanism): the CC manual-family permission
|
|
759
776
|
* modes (default/auto/acceptEdits) gate every fs WRITE hand tool (Write/Edit/NotebookEdit) but never touched
|
|
@@ -831,6 +848,12 @@ export interface ServiceConfigFlat {
|
|
|
831
848
|
* stream (the durable-suspend leg is wired separately). A CC cap → posture-gated (single-user turnkey →
|
|
832
849
|
* ON; multi-tenant opt-in); `ASK_QUESTION_ENABLED=true/false` overrides. */
|
|
833
850
|
askQuestionEnabled: boolean;
|
|
851
|
+
/** #166 裁2:AskUserQuestion 的**裁量窗**——每 run 腿的并发帽 / 总量帽 / 无人应答的释放窗。窗用尽或到期
|
|
852
|
+
* 不是拒绝、也不是替人作答:协调器报 `unavailable`,落点(durable park / 非 durable 合成续跑)归 core。
|
|
853
|
+
* `QUESTION_MAX_CONCURRENT_PER_RUN`(默认 2)/ `QUESTION_MAX_TOTAL_PER_RUN`(20)/ `QUESTION_TTL_MS`
|
|
854
|
+
* (300000),越界值启动期响亮拒(照 `MCP_ELICITATION_*` 同门,不静默夹取)。缺席 ⇒ 协调器出厂窗
|
|
855
|
+
* {@link import("./question.js").DEFAULT_QUESTION_THROTTLE}(与本旋钮族默认逐字同值)。 */
|
|
856
|
+
questionThrottle?: QuestionThrottle;
|
|
834
857
|
/** [816]/[820]② live tool-approval HITL — mount the ToolApprovalCoordinator on `RunnerDeps.onAsk` + the
|
|
835
858
|
* `POST /v1/tool-approvals/:id/respond` route so a policy `ask` (the fs-write gate, ask-lists, non-durable safety
|
|
836
859
|
* asks) routes to the live human as the CC three-choice card on the run's stream (core 1.290 sync-ask leg). A CC
|
|
@@ -975,7 +998,7 @@ export type ServiceStoreConfig = Pick<ServiceConfigFlat, "sessionBackend" | "ses
|
|
|
975
998
|
export type ServiceModelPlaneConfig = Pick<ServiceConfigFlat, "gatewayBaseUrl" | "gatewayApiKey" | "gatewayFallbackUrls" | "gatewayMaxRetries" | "anthropic" | "resilience" | "model" | "models" | "modelApiKeyEnv" | "modelApiKeys" | "modelQuotaWeights" | "tiers" | "projects" | "roles" | "cascadeLadder" | "degrade">;
|
|
976
999
|
/** 组:approval(审批 / HITL 门)。`directDoorActive` 无 env 解析腿(装配层三域合取的产物),但语义上
|
|
977
1000
|
* 就是本组的门状态,故进组;`parseApprovalDomain` 的返回类型相应是 `Omit<…, "directDoorActive">`。 */
|
|
978
|
-
export type ServiceApprovalConfig = Pick<ServiceConfigFlat, "approvalRequire" | "approvalDeny" | "approvalTimeoutSec" | "approvalAutoBudget" | "approvalNeverAuto" | "approvalHmacKeys" | "durableApproval" | "directApprovalDoor" | "directDoorActive" | "resourceSuspend" | "resourceSuspendTtlSec" | "askQuestionEnabled" | "toolApprovalEnabled" | "streamAskWindowMarginMs" | "streamApproval" | "mcpElicitation" | "sensitiveWritePatterns" | "manualModeShellGate">;
|
|
1001
|
+
export type ServiceApprovalConfig = Pick<ServiceConfigFlat, "approvalRequire" | "approvalDeny" | "approvalTimeoutSec" | "approvalAutoBudget" | "approvalNeverAuto" | "approvalHmacKeys" | "durableApproval" | "directApprovalDoor" | "directDoorActive" | "resourceSuspend" | "resourceSuspendTtlSec" | "askQuestionEnabled" | "questionThrottle" | "toolApprovalEnabled" | "streamAskWindowMarginMs" | "streamApproval" | "mcpElicitation" | "sensitiveWritePatterns" | "manualModeShellGate">;
|
|
979
1002
|
/** 组:memory(记忆面 + TOC 同步腿)。 */
|
|
980
1003
|
export type ServiceMemoryConfig = Pick<ServiceConfigFlat, "memoryEngineEnabled" | "memoryEngineDir" | "memoryEngineRemoteLaneAllowed" | "memoryEngineBackend" | "memoryScope" | "memorySync" | "memoryOrgAdmissionMode" | "memoryOrgDirectoryJson" | "memoryOrgGrantTtlMs" | "memoryOrgUnavailableBackoffMs" | "projectMemoryEnabled" | "syncImportLeaseStaleSec">;
|
|
981
1004
|
/** 组:auth(鉴权 / 身份 / 治理棒)。`commandPolicy` 只有 sema-registry 腿(无 env 标量形),故 env 解析
|
package/dist/config.d.ts
CHANGED
|
@@ -137,7 +137,11 @@ export declare function applyAutoCompactWindow(m: Model, explicit?: number): voi
|
|
|
137
137
|
* (常驻进程不 crash);
|
|
138
138
|
* ③ per-request 腿(`settings.permissions.{ask,deny}`,task-settings.ts)—— 该请求 422。
|
|
139
139
|
*
|
|
140
|
-
*
|
|
140
|
+
* 四种拼法:
|
|
141
|
+
* - CC 形规则条目(`Bash(ps:*)`/`Edit(src/**)`,#186 [3047]§七 装机实测):CC 的权限规则 DSL 写法。这些
|
|
142
|
+
* 名单比的是**整串工具名**,带括号那串不是任何活工具的名字 ⇒ 永不匹配。指引=按命令名走
|
|
143
|
+
* `runtime.commandPolicy`,整条 shell 进门走 `MANUAL_MODE_SHELL_GATE=always`。判别式见
|
|
144
|
+
* {@link findCcRuleFormNames}(它同时是 `allow` 腿的**唯一**判据,见下方例外说明)。
|
|
141
145
|
* - 退役名(`bash`/`Task`/`KillShell`…):core `RETIRED_TOOL_NAMES` 静态表,指引=现役名。
|
|
142
146
|
* - pre-prefix 短名(`figma__x`):v4 自动加前缀的折叠面已删,实挂名恒带 `mcp__`,指引=补全前缀。
|
|
143
147
|
* - 不完整 MCP 名(`mcp__`、`mcp__figma`):前缀对但缺段——MCP 实挂名恒是 `mcp__<server>__<tool>` 三段形,
|
|
@@ -151,6 +155,7 @@ export interface UnmatchableToolName {
|
|
|
151
155
|
name: string;
|
|
152
156
|
guidance: string;
|
|
153
157
|
}
|
|
158
|
+
export declare function findCcRuleFormNames(names: readonly string[]): UnmatchableToolName[];
|
|
154
159
|
export declare function findUnmatchableToolNames(names: readonly string[]): UnmatchableToolName[];
|
|
155
160
|
/** {@link findUnmatchableToolNames} 的成句形——三条腿的文案同源(只有前缀/出口不同)。 */
|
|
156
161
|
export declare function formatUnmatchableToolNames(source: string, bad: readonly UnmatchableToolName[]): string;
|
package/dist/config.js
CHANGED
|
@@ -5,6 +5,7 @@ import { CODE_AGENT_PROMPT, formatUserScope, isThinkingLevel, PROTOCOL_TABLE, pr
|
|
|
5
5
|
import { ROSTER_PRIMARY_ROLES, ROSTER_CHEAP_ROLES } from "@sema-agent/registry-core";
|
|
6
6
|
import { parseApprovalHmacKeys, parsePrincipalJwks } from "./auth-keys.js"; // design/158 A4: the parser leaf — NOT security.js (base config layer must not value-import the 55KiB auth module)
|
|
7
7
|
import { DEFAULT_ELICITATION_THROTTLE } from "./elicitation.js";
|
|
8
|
+
import { DEFAULT_QUESTION_THROTTLE } from "./question.js";
|
|
8
9
|
function csv(name) {
|
|
9
10
|
return (process.env[name] ?? "")
|
|
10
11
|
.split(",")
|
|
@@ -878,9 +879,43 @@ function parseDegradeOn(words) {
|
|
|
878
879
|
}
|
|
879
880
|
return words;
|
|
880
881
|
}
|
|
882
|
+
/**
|
|
883
|
+
* #186 — the CC-rule-form arm, split out because it is the ONE arm that also judges an **allow** list.
|
|
884
|
+
*
|
|
885
|
+
* The general "unmatchable name" judgment deliberately spares `allow` (a mistyped live name leaves the allowlist
|
|
886
|
+
* effectively empty ⇒ everything is denied ⇒ fail-CLOSED and immediately visible). A CC-form entry is a different
|
|
887
|
+
* animal: `allow: ["Bash(ps:*)"]` is what a CC user writes to get FEWER prompts, and what they actually get is
|
|
888
|
+
* "only a tool literally named `Bash(ps:*)` may run" ⇒ Bash unavailable, with no warning anywhere. That is a
|
|
889
|
+
* spelling-FAMILY error, not a typo, so it is refused on every list — with the two knobs that really do the job.
|
|
890
|
+
*
|
|
891
|
+
* 本批**不实现** CC 形语义(参数级规则是扩面,另候裁);判据只认括号的在场,残形(`Bash(ps`)同样拒——
|
|
892
|
+
* 半个括号一样永不匹配活名。
|
|
893
|
+
*/
|
|
894
|
+
function ccRuleFormGuidance(name) {
|
|
895
|
+
if (!name.includes("(") && !name.includes(")"))
|
|
896
|
+
return undefined;
|
|
897
|
+
return (`CC-form rule specifier — this list matches the WHOLE tool name verbatim, so it can only ever match a tool literally named "${name}". ` +
|
|
898
|
+
`Drop the parentheses to gate the tool itself ("${name.split("(")[0] ?? name}"); ` +
|
|
899
|
+
`to decide per COMMAND use \`runtime.commandPolicy\` ({command, decision} — bare argv[0] names); ` +
|
|
900
|
+
`to put every shell call behind an approval use MANUAL_MODE_SHELL_GATE=always`);
|
|
901
|
+
}
|
|
902
|
+
export function findCcRuleFormNames(names) {
|
|
903
|
+
const out = [];
|
|
904
|
+
for (const name of names) {
|
|
905
|
+
const guidance = ccRuleFormGuidance(name);
|
|
906
|
+
if (guidance !== undefined)
|
|
907
|
+
out.push({ name, guidance });
|
|
908
|
+
}
|
|
909
|
+
return out;
|
|
910
|
+
}
|
|
881
911
|
export function findUnmatchableToolNames(names) {
|
|
882
912
|
const out = [];
|
|
883
913
|
for (const name of names) {
|
|
914
|
+
const ccGuidance = ccRuleFormGuidance(name);
|
|
915
|
+
if (ccGuidance !== undefined) {
|
|
916
|
+
out.push({ name, guidance: ccGuidance });
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
884
919
|
const retired = RETIRED_TOOL_NAMES.get(name);
|
|
885
920
|
if (retired !== undefined) {
|
|
886
921
|
out.push({ name, guidance: retired });
|
|
@@ -902,26 +937,36 @@ export function findUnmatchableToolNames(names) {
|
|
|
902
937
|
}
|
|
903
938
|
/** {@link findUnmatchableToolNames} 的成句形——三条腿的文案同源(只有前缀/出口不同)。 */
|
|
904
939
|
export function formatUnmatchableToolNames(source, bad) {
|
|
905
|
-
return (`${source} names tool(s) that can never match a live tool (core 5.0.0 RB-476
|
|
940
|
+
return (`${source} names tool(s) that can never match a live tool (these lists are RAW whole-name comparisons — core 5.0.0 RB-476 removed the alias/auto-prefix folds): ` +
|
|
906
941
|
bad.map((b) => `${b.name} → ${b.guidance}`).join("; "));
|
|
907
942
|
}
|
|
908
943
|
/**
|
|
909
944
|
* #151 流内审批协议(design/172)的九键段(车3 刀 3a 五键 + 车5 收敛器四键)。
|
|
910
945
|
*
|
|
911
|
-
* **总开关默认
|
|
912
|
-
*
|
|
946
|
+
* **总开关默认 true**(clay 裁 2026-08-08 走 (a),#164 翻真验证后)。⚠️ 版本坐标:翻转**已在树上生效**,
|
|
947
|
+
* 发布线上它落在 7.4.0 之后的下一个发布(CHANGELOG `Unreleased` 段,预计 7.5.0)—— 所以一台自报 7.4.0
|
|
948
|
+
* 的**已发布** worker 仍是 OFF,而拿 main 构建的 worker 已是 ON。别只按 `/health` 的版本号推默认。显式
|
|
949
|
+
* `STREAM_APPROVAL_ENABLED=false` 是**唯一干净还原键** ⇒ 全链逐字回到翻转前行为(不发
|
|
950
|
+
* approval_request、不落 ask 行、不起收敛器腿、窗仍是 `DEFAULT_APPROVAL_TTL_MS`)。同一裁定把窗默认
|
|
951
|
+
* 从 60s 抬到 300000(= `DEFAULT_APPROVAL_TTL_MS` 的 5min),于是「开协议」不再顺带把窗砍短 ——
|
|
952
|
+
* 当年默认 false + 60s 窗那一版的「谁需要 / 谁被伤 / 什么补偿」成文记录在 `config-types.ts` 的
|
|
953
|
+
* `ServiceConfigFlat.streamApproval` 注里(读它时记得它记的是**翻转前**的前提)。
|
|
954
|
+
* 窗/帽四键沿用 `numEnv` fail-loud 形(坏形启动期炸,不静默折 NaN);
|
|
913
955
|
* 车5 的四键一律 **`numEnvBounded`**(§9 C6:batch 整数 ≥1 有上帽,grace/TTL 非负有界)——它们直接
|
|
914
956
|
* 决定 reaper 每 tick 的库压与「多久算遗孤」,一个手滑的 `0` 或 `1e12` 都是运维事故面。
|
|
915
957
|
*
|
|
916
|
-
* 🔴 跨旋钮不变量(§9 C6,`#157` 三分类 P 族姿势)
|
|
917
|
-
*
|
|
958
|
+
* 🔴 跨旋钮不变量(§9 C6,`#157` 三分类 P 族姿势):**`WINDOW + ADHOC_GRACE < ORPHAN_TTL`**。破坏形
|
|
959
|
+
* **拒启** —— 判据 4 在 `expiresAtMs + ADHOC_GRACE` 触发而 `expiresAtMs ≈ createdAtMs + WINDOW`,判据 5
|
|
960
|
+
* 在 `createdAtMs + ORPHAN_TTL` 触发;不等式不成立时判据 5 会先于判据 4 到,一条 adhoc 腿拿到的归因就成了
|
|
918
961
|
* `orphan_ttl_exceeded`(「遗孤兜底」)而不是 `adhoc_leg_no_durable_domain`(「结构上无对账域」),
|
|
919
|
-
*
|
|
962
|
+
* 审计面从此读不出真实成因。⚠️ **窗那一项不能省**(本注上一版只写 `ADHOC_GRACE < ORPHAN_TTL`,与下方
|
|
963
|
+
* 真校验和它的 codex C7 论证矛盾):只比宽限会漏掉窗,例如 WINDOW 与 ORPHAN_TTL 等长时校验放行但遗孤
|
|
964
|
+
* 兜底必定先到。默认值(300s + 60s vs 7d)自然满足;只有显式改坏才会撞上。
|
|
920
965
|
*/
|
|
921
966
|
function streamApprovalConfig() {
|
|
922
967
|
const cfg = {
|
|
923
|
-
enabled: boolEnv("STREAM_APPROVAL_ENABLED",
|
|
924
|
-
windowMs: numEnv("STREAM_ASK_WINDOW_MS", "
|
|
968
|
+
enabled: boolEnv("STREAM_APPROVAL_ENABLED", true), // clay 裁 2026-08-08 走 (a):#164 翻真验证后默认 ON;false=唯一干净还原键
|
|
969
|
+
windowMs: numEnv("STREAM_ASK_WINDOW_MS", "300000"), // 同裁抬 5min:「窗变短」从翻真里摘出,与 sync 腿既有活卡窗对齐
|
|
925
970
|
replayMax: numEnv("STREAM_APPROVAL_REPLAY_MAX", "50"),
|
|
926
971
|
admitMaxPerTask: numEnv("STREAM_APPROVAL_ADMIT_MAX_PER_TASK", "32"),
|
|
927
972
|
admitMaxPerOwner: numEnv("STREAM_APPROVAL_ADMIT_MAX_PER_OWNER", "256"),
|
|
@@ -998,13 +1043,21 @@ function parseApprovalDomain(ctx) {
|
|
|
998
1043
|
},
|
|
999
1044
|
},
|
|
1000
1045
|
askQuestionEnabled: postureOn("ASK_QUESTION_ENABLED"), // §4④ AskUserQuestion live HITL; posture-gated (single-user → ON)
|
|
1046
|
+
// #166 裁2:AskUserQuestion 裁量窗三旋钮,姿势照 MCP_ELICITATION_* 抄(bounded ⇒ 越界启动期拒,不静默
|
|
1047
|
+
// 夹取)。默认值 = 协调器出厂窗逐字(纯旋钮化,零行为改动)。
|
|
1048
|
+
questionThrottle: {
|
|
1049
|
+
maxConcurrentPerRun: numEnvBounded("QUESTION_MAX_CONCURRENT_PER_RUN", String(DEFAULT_QUESTION_THROTTLE.maxConcurrentPerRun), 1, 64),
|
|
1050
|
+
maxTotalPerRun: numEnvBounded("QUESTION_MAX_TOTAL_PER_RUN", String(DEFAULT_QUESTION_THROTTLE.maxTotalPerRun), 1, 10_000),
|
|
1051
|
+
ttlMs: numEnvBounded("QUESTION_TTL_MS", String(DEFAULT_QUESTION_THROTTLE.ttlMs), 1_000, 3_600_000),
|
|
1052
|
+
},
|
|
1001
1053
|
toolApprovalEnabled: postureOn("TOOL_APPROVAL_ENABLED"), // [816]/[820]② live tool-approval HITL; posture-gated (single-user → ON), mirrors askQuestion
|
|
1002
1054
|
// #151 车2(design/172 §3.3 D3):ToolApprovalCoordinator 窗长三元的安全余量。numEnv fail-loud 形,
|
|
1003
1055
|
// 照邻居旋钮(approvalTimeoutSec 等)抄——坏形(非数字)在启动期炸,不静默折成 NaN。
|
|
1004
1056
|
streamAskWindowMarginMs: numEnv("STREAM_ASK_WINDOW_MARGIN_MS", "10000"),
|
|
1005
|
-
// #151 车3 刀 3a(design/172 流内审批协议)
|
|
1006
|
-
//
|
|
1007
|
-
//
|
|
1057
|
+
// #151 车3 刀 3a + 车5(design/172 流内审批协议):九键。默认极性 / 窗值 / 解析形 / 跨旋钮不变量
|
|
1058
|
+
// **一处成文** = 本文件 `streamApprovalConfig()` 的头注,这里不复述 —— 复述的两份必然各自漂,这条注
|
|
1059
|
+
// 的上一版正是这么漂的(默认翻 ON 之后它还写着「总开关默认 false」整整一版)。
|
|
1060
|
+
// 三问记录(翻转前那一版)见 config-types.ts 的 StreamApprovalConfig。
|
|
1008
1061
|
streamApproval: streamApprovalConfig(),
|
|
1009
1062
|
// [875]a 成文:0(缺省)= durable HITL 无限期等人,时间型 reapSuspended 不跑;file/memory lane 的回收
|
|
1010
1063
|
// 探针只在 DURABLE_APPROVAL=true 时注入(无 durable 的部署两只 suspended 回收器恒 NO-OP,parked 行的
|
|
@@ -1483,7 +1536,7 @@ const MODEL_PLANE_GROUP_KEYS = [
|
|
|
1483
1536
|
const APPROVAL_GROUP_KEYS = [
|
|
1484
1537
|
"approvalRequire", "approvalDeny", "approvalTimeoutSec", "approvalAutoBudget", "approvalNeverAuto",
|
|
1485
1538
|
"approvalHmacKeys", "durableApproval", "directApprovalDoor", "directDoorActive", "resourceSuspend",
|
|
1486
|
-
"resourceSuspendTtlSec", "askQuestionEnabled", "toolApprovalEnabled", "streamAskWindowMarginMs", "streamApproval", "mcpElicitation",
|
|
1539
|
+
"resourceSuspendTtlSec", "askQuestionEnabled", "questionThrottle", "toolApprovalEnabled", "streamAskWindowMarginMs", "streamApproval", "mcpElicitation",
|
|
1487
1540
|
"sensitiveWritePatterns", "manualModeShellGate",
|
|
1488
1541
|
];
|
|
1489
1542
|
const MEMORY_GROUP_KEYS = [
|
package/dist/elicitation.js
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
28
28
|
import { uuidv7, delimitUntrusted } from "@sema-agent/core";
|
|
29
29
|
import { redactSecrets, redactDeep } from "./trace/redact.js";
|
|
30
|
+
import { recordFailOpen } from "./observability/fail-open.js";
|
|
30
31
|
/** Size bounds on the UNTRUSTED server-controlled payload (the throttle bounds COUNT, these bound SIZE — an
|
|
31
32
|
* opted-in-but-hostile server otherwise amplifies a multi-MB prompt/schema into the durable log + SSE socket).
|
|
32
33
|
* The message is a human-facing prompt (a few KB is ample); a requestedSchema over the cap is dropped (the form
|
|
@@ -187,6 +188,8 @@ export class ElicitationCoordinator {
|
|
|
187
188
|
rc.lastByServer.set(req.server, t);
|
|
188
189
|
}
|
|
189
190
|
catch {
|
|
191
|
+
// #157:投递失败 ⇒ decline。方向已是 fail-closed(问不到人就是拒),缺的只是留痕。
|
|
192
|
+
recordFailOpen("server.elicitation.open-frame-undelivered", `elicitation=${id} server=${req.server}`);
|
|
190
193
|
settle({ action: "decline" }); // inflight rolled back in settle
|
|
191
194
|
}
|
|
192
195
|
const answer = await answerP;
|
|
@@ -195,7 +198,8 @@ export class ElicitationCoordinator {
|
|
|
195
198
|
// ordering-uncritical: the shell already has the answer via respond; a late/lost dismiss is cosmetic.
|
|
196
199
|
void Promise.resolve()
|
|
197
200
|
.then(() => ctx.emit({ type: "elicitation_complete", elicitationId: id, mcpServerName: req.server, action: answer.action, serverNowMs: this.now() }))
|
|
198
|
-
.
|
|
201
|
+
// #157:F 类兜底,与 question.ts 的孪生面同形同 tag 形(丢收尾渲染,不丢答案)。
|
|
202
|
+
.catch(() => recordFailOpen("server.elicitation.complete-breadcrumb-dropped", `elicitation=${id}`));
|
|
199
203
|
return answer;
|
|
200
204
|
};
|
|
201
205
|
/** `POST /v1/elicitations/:id/respond` — resolve a parked elicitation with the shell's answer. Owner-gated with a
|
package/dist/env-facts.d.ts
CHANGED
|
@@ -78,7 +78,9 @@ export declare function ensureScratchpadDir(localDataRoot: string, sessionId: st
|
|
|
78
78
|
* - 绝对路径 + ≤{@link MAX_PATH}(core 渲染上限同源,超限=core 只能给模型假路径)+ canonical
|
|
79
79
|
* 深度 ≥3(裸 /、/tmp、/home 一整块系统目录不能当豁免域;穿越形按 resolve 归一后判);
|
|
80
80
|
* - mkdir -p 确保存在(gate 的 canonicalize 与 core 根围栏都要真目录)。
|
|
81
|
-
*
|
|
81
|
+
* 敏感路径的最后一道**不在**写门折叠里(#177 起搬家):守卫集 `SENSITIVE_WRITE_PATTERNS` 由
|
|
82
|
+
* governance 拍(boot/resolve-spec.ts → applyRuntimeGovernance)无条件铸成 DENY 基线,经 tightenTaskSpec
|
|
83
|
+
* 与写门 deny-wins 折叠 —— 结论不变(deny 恒赢,本函数放出的 exemptDirs 越不过它),但施加点已换。 */
|
|
82
84
|
export declare function acceptShellScratchpadDir(raw: unknown, opts: {
|
|
83
85
|
requirePrincipal: boolean;
|
|
84
86
|
hostSemanticsLane: boolean;
|
package/dist/env-facts.js
CHANGED
|
@@ -113,7 +113,9 @@ export async function ensureScratchpadDir(localDataRoot, sessionId) {
|
|
|
113
113
|
* - 绝对路径 + ≤{@link MAX_PATH}(core 渲染上限同源,超限=core 只能给模型假路径)+ canonical
|
|
114
114
|
* 深度 ≥3(裸 /、/tmp、/home 一整块系统目录不能当豁免域;穿越形按 resolve 归一后判);
|
|
115
115
|
* - mkdir -p 确保存在(gate 的 canonicalize 与 core 根围栏都要真目录)。
|
|
116
|
-
*
|
|
116
|
+
* 敏感路径的最后一道**不在**写门折叠里(#177 起搬家):守卫集 `SENSITIVE_WRITE_PATTERNS` 由
|
|
117
|
+
* governance 拍(boot/resolve-spec.ts → applyRuntimeGovernance)无条件铸成 DENY 基线,经 tightenTaskSpec
|
|
118
|
+
* 与写门 deny-wins 折叠 —— 结论不变(deny 恒赢,本函数放出的 exemptDirs 越不过它),但施加点已换。 */
|
|
117
119
|
export async function acceptShellScratchpadDir(raw, opts) {
|
|
118
120
|
if (typeof raw !== "string" || raw.length === 0)
|
|
119
121
|
return undefined; // additive 常态:键缺席/异形,零告警
|
|
@@ -318,7 +318,12 @@ export declare class FleetEventBus {
|
|
|
318
318
|
type: "snapshot";
|
|
319
319
|
}>;
|
|
320
320
|
/** Subscribe to deltas. Returns an unsubscribe thunk. The caller typically sends `snapshot()` first, then every
|
|
321
|
-
* delta this delivers — the snapshot+delta sequence is the live fleet view.
|
|
321
|
+
* delta this delivers — the snapshot+delta sequence is the live fleet view.
|
|
322
|
+
*
|
|
323
|
+
* 每个回调各自隔离(#183 复审 R3 HIGH):扇出是同步的,修前一个抛错的订阅方会把异常传回**发布方**——
|
|
324
|
+
* 而发布方是 JournalingWorkflowRunStore.put/update 的投影点,core 持久化对 put 抛错 catch{} 且不推进
|
|
325
|
+
* storeRev ⇒ durable 行冻在 running 而 notify 已 ack,无恢复路径。投影消费方永远不该能楔死 durable
|
|
326
|
+
* 写路;抛错记 {@link recordFailOpen}(F 类,登记闸),不上抛、不影响其余订阅方。 */
|
|
322
327
|
subscribe(cb: (frame: FleetFrame) => void): () => void;
|
|
323
328
|
/** Live counts (test/observability). */
|
|
324
329
|
get size(): {
|
package/dist/fleet/fleet-bus.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* replica-local). A cross-replica fleet roll-up is a fleet-token-gated trace-API concern (separate), not this.
|
|
22
22
|
*/
|
|
23
23
|
import { EventEmitter } from "node:events";
|
|
24
|
+
import { recordFailOpen } from "../observability/fail-open.js";
|
|
24
25
|
import { redactSecrets } from "../trace/redact.js";
|
|
25
26
|
/** [2687-cli] 幽灵行案的单源判别:一条 `task_notification` 只有在 **agent 族 × 终态** 时才允许打
|
|
26
27
|
* `onChildTerminal`(fleet「subagent 树」只渲 agent 子代)。`background_bash`/`monitor`/`external`
|
|
@@ -121,10 +122,31 @@ export class FleetEventBus {
|
|
|
121
122
|
return { type: "snapshot", tasks: [...this.tasks.values()], workflows: [...this.workflows.values()], ts: this.now() };
|
|
122
123
|
}
|
|
123
124
|
/** Subscribe to deltas. Returns an unsubscribe thunk. The caller typically sends `snapshot()` first, then every
|
|
124
|
-
* delta this delivers — the snapshot+delta sequence is the live fleet view.
|
|
125
|
+
* delta this delivers — the snapshot+delta sequence is the live fleet view.
|
|
126
|
+
*
|
|
127
|
+
* 每个回调各自隔离(#183 复审 R3 HIGH):扇出是同步的,修前一个抛错的订阅方会把异常传回**发布方**——
|
|
128
|
+
* 而发布方是 JournalingWorkflowRunStore.put/update 的投影点,core 持久化对 put 抛错 catch{} 且不推进
|
|
129
|
+
* storeRev ⇒ durable 行冻在 running 而 notify 已 ack,无恢复路径。投影消费方永远不该能楔死 durable
|
|
130
|
+
* 写路;抛错记 {@link recordFailOpen}(F 类,登记闸),不上抛、不影响其余订阅方。 */
|
|
125
131
|
subscribe(cb) {
|
|
126
|
-
|
|
127
|
-
|
|
132
|
+
const isolated = (frame) => {
|
|
133
|
+
try {
|
|
134
|
+
cb(frame);
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
// detail 构造自身也包住:抛出值若带会抛错的 toString,String() 本身会抛并逃出隔离层(fresh 复审观察项)。
|
|
138
|
+
let detail = "subscriber threw a value whose String() also threw";
|
|
139
|
+
try {
|
|
140
|
+
detail = String(err);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
/* keep the fallback detail */
|
|
144
|
+
}
|
|
145
|
+
recordFailOpen("server.fleet.subscriber-callback-threw", detail);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
this.emitter.on("frame", isolated);
|
|
149
|
+
return () => this.emitter.off("frame", isolated);
|
|
128
150
|
}
|
|
129
151
|
/** Live counts (test/observability). */
|
|
130
152
|
get size() {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** 标记通道的活对象(CLAUDE.md 工厂命名律:有方法 ⇒ `create*`)。**一格 scope 的视图**。 */
|
|
2
|
+
export interface GovernanceAskMarks {
|
|
3
|
+
/**
|
|
4
|
+
* 写下「这次工具调用的门**当前**是不是治理层给的」。
|
|
5
|
+
*
|
|
6
|
+
* 🔴 为什么是 set 而不是只加不减(codex 交叉复审 round2 [medium],验真):**同一个 `toolCallId` 会被
|
|
7
|
+
* 复查**。core 的 `recheckApprovedEdit`(`prepare-task.js`)在人编辑了 args 之后拿**同一个**
|
|
8
|
+
* `creq.toolCallId` 重跑策略链,并可能就此再产一只 ask。若只加不减,「原始 args 治理层要问 / 编辑后
|
|
9
|
+
* 治理层放行、由**别的**策略提出第二问」这条真实路径会让第二张卡挂着一个陈旧的治理标 —— 而
|
|
10
|
+
* `isMarked` 又是非消费式的,这个假出处会一直传下去。所以每次治理层裁决都**如实覆写**本格。
|
|
11
|
+
*/
|
|
12
|
+
setMarked(toolCallId: string, marked: boolean): void;
|
|
13
|
+
/** 回查(**非消费式**)。空串/缺席一律 false —— 没有身份就没有证据。 */
|
|
14
|
+
isMarked(toolCallId: string | undefined): boolean;
|
|
15
|
+
/** 测试用:清掉本视图能看到的格。 */
|
|
16
|
+
clear(): void;
|
|
17
|
+
/** 测试/可观测:本视图内的标记数。 */
|
|
18
|
+
size(): number;
|
|
19
|
+
}
|
|
20
|
+
/** 一张独立的标记表(不进程共享)。生产路径用的是下面 `governanceAskMarksFor` 给出的**分格视图**;
|
|
21
|
+
* 本工厂给测试(与将来的多实例形)一个不依赖任何环境的干净表。 */
|
|
22
|
+
export declare function createGovernanceAskMarks(opts?: {
|
|
23
|
+
max?: number;
|
|
24
|
+
}): GovernanceAskMarks;
|
|
25
|
+
/** 进入一条 run 腿的标记作用域(唯一调用点 = `ToolApprovalCoordinator.runWithContext`)。 */
|
|
26
|
+
export declare function runWithGovernanceAskScope<T>(scope: string, fn: () => T): T;
|
|
27
|
+
/** **写侧**入口:当前异步作用域那一格的视图;作用域外 = `undefined`(写 no-op,见顶注)。 */
|
|
28
|
+
export declare function currentGovernanceAskMarks(): GovernanceAskMarks | undefined;
|
|
29
|
+
/** **读侧**入口:按**显式** scope 取视图(读侧不读 ALS —— 理由见顶注 round5 那段)。 */
|
|
30
|
+
export declare function governanceAskMarksFor(scope: string): GovernanceAskMarks;
|
|
31
|
+
//# sourceMappingURL=governance-ask-marks.d.ts.map
|
|
@@ -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 插值(配置本身绝不携带密钥值,契约同边界)。
|