@sema-agent/server 7.1.0 → 7.3.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.
Files changed (76) hide show
  1. package/README.md +3 -1
  2. package/README.zh-CN.md +1 -1
  3. package/USAGE.md +6 -1
  4. package/dist/approval-ask-machine.d.ts +39 -0
  5. package/dist/approval-ask-machine.js +101 -0
  6. package/dist/approval-card.d.ts +244 -0
  7. package/dist/approval-card.js +237 -0
  8. package/dist/approval-deny-reasons.d.ts +56 -0
  9. package/dist/approval-deny-reasons.js +54 -0
  10. package/dist/approval-reconciler.d.ts +167 -0
  11. package/dist/approval-reconciler.js +307 -0
  12. package/dist/boot/coordinators.d.ts +1 -0
  13. package/dist/boot/coordinators.js +36 -3
  14. package/dist/boot/lexical-path-env.d.ts +14 -0
  15. package/dist/boot/lexical-path-env.js +116 -0
  16. package/dist/boot/org-memory.d.ts +8 -5
  17. package/dist/boot/org-memory.js +23 -12
  18. package/dist/boot/reapers.d.ts +34 -0
  19. package/dist/boot/reapers.js +198 -23
  20. package/dist/boot/resolve-spec.d.ts +16 -1
  21. package/dist/boot/resolve-spec.js +104 -46
  22. package/dist/config-center/facade.d.ts +1 -1
  23. package/dist/config-center/facade.js +1 -1
  24. package/dist/config-center/http-client.d.ts +19 -0
  25. package/dist/config-center/http-client.js +81 -32
  26. package/dist/config-types.d.ts +76 -7
  27. package/dist/config.d.ts +1 -0
  28. package/dist/config.js +134 -4
  29. package/dist/elicitation.d.ts +4 -0
  30. package/dist/elicitation.js +2 -2
  31. package/dist/http/routes/capabilities.js +18 -2
  32. package/dist/http/routes/runs.d.ts +1 -0
  33. package/dist/http/routes/runs.js +548 -16
  34. package/dist/http/routes/tasks.js +190 -12
  35. package/dist/http/server.d.ts +1 -1
  36. package/dist/http/server.js +124 -8
  37. package/dist/http/sse-log.d.ts +51 -0
  38. package/dist/http/sse-log.js +64 -0
  39. package/dist/http/wire-types.d.ts +20 -5
  40. package/dist/leader/wire.js +4 -0
  41. package/dist/main.js +7 -4
  42. package/dist/org-memory-admission.d.ts +2 -1
  43. package/dist/org-memory-admission.js +10 -4
  44. package/dist/parked-decide.js +5 -2
  45. package/dist/plugins/approval-ask-store-memory.d.ts +38 -0
  46. package/dist/plugins/approval-ask-store-memory.js +299 -0
  47. package/dist/plugins/approval-ask-store-sql.d.ts +341 -0
  48. package/dist/plugins/approval-ask-store-sql.js +705 -0
  49. package/dist/plugins/background-agent-store-sql.js +20 -1
  50. package/dist/plugins/checkpoint-store-sql.d.ts +84 -9
  51. package/dist/plugins/checkpoint-store-sql.js +297 -16
  52. package/dist/plugins/local-checkpoint-store.d.ts +6 -5
  53. package/dist/plugins/local-checkpoint-store.js +4 -0
  54. package/dist/plugins/pg-pool.js +11 -0
  55. package/dist/plugins/store-backend.d.ts +18 -0
  56. package/dist/plugins/store-backend.js +10 -0
  57. package/dist/plugins/tidb-pool.js +27 -4
  58. package/dist/question.d.ts +18 -3
  59. package/dist/question.js +20 -6
  60. package/dist/runs.d.ts +16 -1
  61. package/dist/runs.js +61 -3
  62. package/dist/runtime-caps-resolver.d.ts +7 -1
  63. package/dist/runtime-caps-resolver.js +65 -3
  64. package/dist/runtime-governance.d.ts +11 -4
  65. package/dist/runtime-governance.js +16 -5
  66. package/dist/spec-fields.d.ts +4 -0
  67. package/dist/spec-fields.js +6 -0
  68. package/dist/task-settings.d.ts +35 -15
  69. package/dist/task-settings.js +19 -5
  70. package/dist/tool-approval.d.ts +296 -3
  71. package/dist/tool-approval.js +1066 -50
  72. package/dist/trace/core-keyset-guard.d.ts +2 -2
  73. package/dist/trace/ledger-sink.js +14 -1
  74. package/dist/trace/project.d.ts +55 -0
  75. package/dist/trace/project.js +135 -0
  76. package/package.json +4 -3
@@ -42,19 +42,26 @@ import { applyRuntimeGovernance, stripDelegationTools } from "../runtime-governa
42
42
  import { HttpError, encodeCheckpointScope } from "../security.js";
43
43
  import { memorySpecForRequest } from "../memory-scope.js";
44
44
  import { mcpForScenario } from "../config-center/facade.js";
45
- import { normalizeAttachments, normalizeResilience, normalizeResumeAtMode, normalizeSuggestNextPrompts, promptProfileFromBody, resolveTaskLimits, retainBackgroundProcessesFromBody, taskAgentsSpecFragment, toolNameListFromBody } from "../spec-fields.js";
45
+ import { normalizeAttachments, normalizeResilience, normalizeResumeAtMode, normalizeSuggestNextPrompts, promptProfileFromBody, resolveTaskLimits, retainBackgroundProcessesFromBody, taskAgentsSpecFragment, toolMaterializeStrategyFromBody, toolNameListFromBody } from "../spec-fields.js";
46
46
  import { cwdHonored, effectiveHostWorkspace, inProcessSingleUserLane, isValidCwd, parseAdditionalDirectories, satisfiedByProcessCwd, shellEnvMismatchCount } from "../task-cwd.js";
47
47
  import { resolveRequestMcp } from "../task-mcp.js";
48
48
  import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, acceptAppendSystemPrompt, applyTaskSettings, coercePermissionMode, effectiveThinking, hasConstitutionAnchors, parseTaskSettings, providerDropsAppend, withPermissionMode } from "../task-settings.js";
49
49
  import { enableForkFromBody, normalizeRetainSubagentSessions, selfOrchestrationFromBody } from "../task-workflow.js";
50
50
  import { redactSecrets } from "../trace/redact.js";
51
+ import { LexicalPathExecutionEnv } from "./lexical-path-env.js";
51
52
  /** #152([2703] 案二):durable 部署上的 AskUserQuestion 门。活体面(QuestionCoordinator)缺席 ⇒ 原形
52
53
  * `createDurableQuestionPolicy()`(恒 ask ⇒ 恒 durable park)。在场 ⇒ **判决时**按活流上下文分腿:
53
- * 活流腿(bg/SSE,coordinator.runWithContext 包裹,ALS 判)allow——工具执行落到 RunnerDeps.onQuestion
54
- * 的 coordinator,问正在 tail 流的活人;无活流腿(sync /v1/tasks、verify/cascade)ask——durable park
55
- * 原语义逐字保留(此时执行只会拿到 headless 空答,park 才是把问题送到人面前的那条腿)。
56
- * 工具名字面量与 server.ts 的 pre-CAS 守卫同源("AskUserQuestion",core 未根导出常量)。 */
57
- function durableQuestionPolicy(live) {
54
+ * 活流腿(bg/SSE,coordinator.runWithContext 包裹且投递面此刻可达,ALS 判)allow——工具执行落到
55
+ * RunnerDeps.onQuestion 的 coordinator,问正在 tail 流的活人;无活流腿(sync /v1/tasks、verify/cascade
56
+ * durable resume 驱动、断连后的 detach 腿)ask——durable park 原语义逐字保留(此时执行只会拿到 headless
57
+ * 空答,park 才是把问题送到人面前的那条腿)。
58
+ * 工具名字面量与 server.ts 的 pre-CAS 守卫同源("AskUserQuestion",core 未根导出常量)
59
+ * ⚠️ **单一属主**(复审 A2):AskUserQuestion 的 durable 判决只有这一处。任何需要「同参重建」这条判决的
60
+ * 地方(main.ts 的 parkedReviveInheritedGate 父约束链)必须调本工厂,不得自折 core 原形——两份拷贝里
61
+ * 只改一份正是本条 finding 的成因。**登记豁免一处**:leader worker 腿(src/leader/wire.ts provisionWorker)
62
+ * 自折 core 原形——该腿无活体问答面可装且 leader 不 import boot 层(分层),core 原形+sentinel 即其完整
63
+ * 语义;豁免注在彼处互指,接活体面之日必须并回本工厂。 */
64
+ export function createDurableQuestionGate(live) {
58
65
  if (live === undefined)
59
66
  return createDurableQuestionPolicy();
60
67
  return {
@@ -102,7 +109,11 @@ export function createResolveSpec(ctx) {
102
109
  // allowedLanes ⇒ typed 403 `execution_lane_not_allowed`. Same caps car as the scenario ruling (zero extra
103
110
  // RTT); fail-open + audit on blip. "in-process" = fleet's REMOTE_EXEC-unset core stub lane
104
111
  // (named distinctly from the explicit `host` lane — same machine, different posture).
105
- gateExecutionLane(await principalCaps?.executionRuling(auth?.principal), config.remoteExec?.provider ?? "in-process");
112
+ // #157 补痕交叉复审真发现(codex,2026-08-05):第三参不传就落 runtime-caps-resolver.ts 自己的私有
113
+ // `defaultLogger`(createLogger() 默认 minLevel="info")——与本部署真正按 config.logLevel 建的 `logger`
114
+ // 是两个独立实例,即便运维把 LOG_LEVEL 调到 debug 也打不到这条新增的一次性默认落痕行(补痕功能形同虚设)。
115
+ // 显式传本装配的真 logger,让该痕迹真的受部署日志级别管辖。
116
+ gateExecutionLane(await principalCaps?.executionRuling(auth?.principal), config.remoteExec?.provider ?? "in-process", logger);
106
117
  // Resolve an unknown name to "default" so the WHOLE bundle stays coherent — tools/prompt AND
107
118
  // skills/mcp fall back together. (selectScenario already falls back for tools/prompt; without
108
119
  // resolving here, skills/mcp would still be filtered by the raw unknown name → any skill/MCP tagged
@@ -553,6 +564,10 @@ export function createResolveSpec(ctx) {
553
564
  // [1144]/[1146] (core 1.328 R2): promptProfile 双形轴直通——纯呈现轴无租户门(deferTools 姿势);
554
565
  // 缺省不挂键(引擎缺省 simple);非法值由 HTTP 门 400,resume 重放 defensive DROP。
555
566
  ...(() => { const v = promptProfileFromBody(body.promptProfile); return v ? { promptProfile: v } : {}; })(),
567
+ // [2856]②(core 5.14.0):deferred 工具激活后的 schema 供给策略,per-task 位。core 侧读法是
568
+ // `spec.toolMaterializeStrategy ?? env ?? "static"` ⇒ **缺省不挂键**才能让进程级 env 继续说话;
569
+ // 挂一个「缺省值」上去会把 env 车道悄悄夺权。非法值由 HTTP 门 400,resume 重放 defensive DROP。
570
+ ...(() => { const v = toolMaterializeStrategyFromBody(body.toolMaterializeStrategy); return v ? { toolMaterializeStrategy: v } : {}; })(),
556
571
  // [922]①/[915]② (core 1.296): interactiveTools 三态旋钮直通——boolean 上 spec(壳 -p 恒 stamp false=
557
572
  // A3 类「headless 提问 park」根治腿;plan 显式 true 恒赢),undefined=core 自动判据(送达面探测)。
558
573
  // per-run 工具面选择无跨租户面 ⇒ 无租户门;非 boolean 由 HTTP 门 400,resume 重放 typeof 检查天然 DROP。
@@ -736,7 +751,7 @@ export function createResolveSpec(ctx) {
736
751
  // policy is LIVE-AWARE when the QuestionCoordinator is wired — a live-stream leg adjudicates `allow`
737
752
  // (the live human answers over the stream) instead of unconditionally parking; see durableQuestionPolicy.
738
753
  toolPolicy: durableEnabled
739
- ? combinePolicies(durableQuestionPolicy(liveQuestionFace), createDurableAskPolicy({
754
+ ? combinePolicies(createDurableQuestionGate(liveQuestionFace), createDurableAskPolicy({
740
755
  requireApproval: config.approvalRequire, deny: config.approvalDeny, autoBudget: config.approvalAutoBudget, neverAuto: config.approvalNeverAuto,
741
756
  // The probe key is the CONTINUED session (auth.sessionId — the same id that keys the
742
757
  // durable checkpoint /decide route). A fresh session (no body.sessionId) has no exemptions by
@@ -864,12 +879,24 @@ export function createResolveSpec(ctx) {
864
879
  const scratchpadDir = shellScratchpad ??
865
880
  (hostSemanticsLane && auth?.sessionId ? await ensureScratchpadDir(config.localDataRoot ?? localRoot, auth.sessionId) : undefined);
866
881
  if (effectiveSettings) {
867
- // [816]/[820]① fs-write gate wiring HOST-semantics lanes only: core's createFsWriteGatePolicy
868
- // canonicalizes每一个 target/dir 走给定 env 的真实 fs(exists/canonicalPath/readLink,dist 亲读),所以
869
- // env 必须就是 hand 工具真正写的那块盘。e2b/k8s/ssh/adb/local-docker 的沙箱 env core spec 之后
870
- // 才铸(executionEnvFactory),这里给 worker 本机 env 会拿错误的 fs 裁决(symlink/exists 全答错)——
871
- // 沙箱 lane 诚实不挂(gate=undefined derive 回落 base 规则,pre-[816] 行为)。
872
- // NodeExecutionEnv 构造是纯字段赋值(不 spawn)。
882
+ // [816]/[820]① fs-write gate wiring —— 两形,按 lane 分:
883
+ //
884
+ // HOST-semantics lane(REMOTE_EXEC 未设或 "host"):core createFsWriteGatePolicy 把每一个
885
+ // target/dir 交给给定 env 的**真实 fs**(exists/canonicalPath/readLink,dist 亲读),所以 env 必须
886
+ // 就是 hand 工具真正写的那块盘 —— 这里给 worker 本机的 NodeExecutionEnv(构造是纯字段赋值,不 spawn)。
887
+ //
888
+ // ② 沙箱 lane(e2b/k8s/ssh/adb/local-docker,#156):沙箱 env 由 core 在 spec **之后**才铸
889
+ // (executionEnvFactory),spec 期把 worker 本机 env 递进去会拿**别人机器上的 fs 事实**裁决
890
+ // (symlink/exists 全答错),而错误的 allow 比没有门更糟 —— 这就是 [816] 当年在这些 lane 上诚实
891
+ // 不挂门的理由(#153 的 ③ 号注把它记成余款)。过渡还款不是把本机 fs 递进去,而是换一个**没有 fs**
892
+ // 的裁决 env:LexicalPathExecutionEnv 只对 POSIX 绝对路径做词法归一,其余(相对路径/`~`/Windows 形)
893
+ // 一律「解析不出」⇒ core 落 ask。于是沙箱 lane 拿到「绝对路径按词法判、其余 fail-closed」的写门,
894
+ // sensitivePatterns 的 deny 腿(按路径段词法匹配)与 isExempt 会话豁免探针(name-keyed,不做 fs
895
+ // 裁决)照常施加。**一个自动放行域都不铸**——两个候选域(cwd / scratchpad)各自的理由与代价写在
896
+ // 下面那条臂的注里,合起来的效果是 acceptEdits 在沙箱 lane 退化成与 default 同形(全 ask),与本段
897
+ // ① 里 host factory lane 的哨兵先例同一个 fail-safe 方向。
898
+ // 已知残余面(symlink 形看不见)与终局 seam(core [2751] 给 HookToolContext 加只读 env 能力窄面,
899
+ // 排 174/#22 窗;到货后本过渡形整体退役换正解)成文在 lexical-path-env.ts 的文件头。
873
900
  //
874
901
  // 修5(三路复审 W4,cwd 基准漂移):gate 的 cwd 基准=与 hands 真跑处同源,分三形:
875
902
  // · session 注册过 cwd(cwdHonored 已在注册处把关)⇒ 用它——与 host factory 的 effectiveHostWorkspace
@@ -881,6 +908,32 @@ export function createResolveSpec(ctx) {
881
908
  // (hand 工具的书面契约形)与 scratchpad exempt 均不受影响。
882
909
  // · provider 未设(in-process host / run-local)⇒ hands 就在本进程 cwd 跑(无 factory),process.cwd()
883
910
  // 正是真工作目录,保留。
911
+ // 两形**共有**的两件(都与 env/cwd 无关,故不随 lane 分叉——单一属主,两臂各抄一份正是漂移的成因):
912
+ const gateCommon = {
913
+ // ③ (core 1.295) sensitive-path write deny set:组合进 gate 腿的同一 combinePolicies 折叠
914
+ // (deny 恒赢 —— 豁免/acceptDirs 越不过;集合取舍 core 成文,server 只透传 config 旋钮:
915
+ // 缺省 = core RECOMMENDED_SENSITIVE_PATTERNS,SENSITIVE_WRITE_PATTERNS 显式替换/off 关闭)。
916
+ // 沙箱 lane 上它按**词法** key 的路径段匹配(#156)——glob 本就只看段名,不需要真 fs。
917
+ ...(config.sensitiveWritePatterns.length > 0 ? { sensitivePatterns: config.sensitiveWritePatterns } : {}),
918
+ // [1557]§四 的 shellGate 旧家已随 #153 搬到 applyRuntimeGovernance(governance 层无条件施加,
919
+ // 见上方 governedBase)——wiring 不再承运该旋钮,FsWriteGateWiring.shellGate 字段一并拆除。
920
+ // [841]① / core 1.294 exemption seam:gate 产 ask 前查同一只 approval_exemption 店(与 decide
921
+ // remember="session" 的授予、ask 政策层探针同店同 canonical toolName 键空间)——「本会话不再询问」
922
+ // 对 fs-write 门同 turn 即时生效(父+继承子任务,ask 路径不再重入)。has() reject ⇒ core 按
923
+ // 未豁免处理(fail-closed 照 ask),与店契约的调用方纪律一致;命中打 info 审计(对齐
924
+ // approval_exempted 的短路留痕,不记路径,路径留在 core 的 allow message 里)。探针是 name-keyed
925
+ // 的(不做任何 fs 裁决),所以沙箱 lane 照接不误。
926
+ ...(approvalExemptionStore && auth?.sessionId
927
+ ? {
928
+ isExempt: async (toolName) => {
929
+ const hit = await approvalExemptionStore.has(auth.sessionId, toolName);
930
+ if (hit)
931
+ logger.info("fs_write_gate_exempted", { toolName, sessionId: auth.sessionId });
932
+ return hit;
933
+ },
934
+ }
935
+ : {}),
936
+ };
884
937
  const fsWriteGate = hostSemanticsLane
885
938
  ? (() => {
886
939
  const sessionCwd = auth?.sessionId ? effectiveHostWorkspace(perSessionCwd.get(auth.sessionId), {}) : undefined;
@@ -892,32 +945,37 @@ export function createResolveSpec(ctx) {
892
945
  env: new NodeExecutionEnv({ cwd: gateCwd }),
893
946
  cwd: gateCwd,
894
947
  ...(scratchpadDir ? { scratchpadDir } : {}),
895
- // ③ (core 1.295) sensitive-path write deny set:组合进 gate 腿的同一 combinePolicies 折叠
896
- // (deny 恒赢 —— 豁免/acceptDirs 越不过;集合取舍 core 成文,server 只透传 config 旋钮:
897
- // 缺省 = core RECOMMENDED_SENSITIVE_PATTERNS,SENSITIVE_WRITE_PATTERNS 显式替换/off 关闭)
898
- ...(config.sensitiveWritePatterns.length > 0 ? { sensitivePatterns: config.sensitiveWritePatterns } : {}),
899
- // [1557]§四 shellGate 旧家已随 #153 搬到 applyRuntimeGovernance(governance 层无条件施加,
900
- // 见上方 governedBase)——wiring 不再承运该旋钮,FsWriteGateWiring.shellGate 字段一并拆除。
901
- // [841]① / core 1.294 exemption seam:gate 产 ask 前查同一只 approval_exemption 店(与 decide
902
- // remember="session" 的授予、ask 政策层探针同店同 canonical toolName 键空间)——「本会话不再询问」
903
- // 对 fs-write 门同 turn 即时生效(父+继承子任务,ask 路径不再重入)。has() reject core
904
- // 未豁免处理(fail-closed 照 ask),与店契约的调用方纪律一致;命中打 info 审计(对齐
905
- // approval_exempted 的短路留痕,不记路径,路径留在 core 的 allow message 里)。
906
- ...(approvalExemptionStore && auth?.sessionId
948
+ ...gateCommon,
949
+ };
950
+ })()
951
+ : // 沙箱 lane(#156):无 fs 的词法裁决 env,且**刻意不给** cwd scratchpadDir —— 两者都是
952
+ // 「自动放行域」,而在纯词法规则下一个放行域就是本设计唯一可能产生**错误 allow** 的地方
953
+ // (词法看不见 symlink:域内一个指向域外的软链会被判成域内)。其余裁决只会是 ask 或 deny。
954
+ //
955
+ // ⚠️ 沙箱侧 scratchpad 的对应物**确实存在**(plugins/remote-scratchpad.ts:`/tmp/scratchpad/<sessionId>`,
956
+ // e2b/k8s/local-docker/ssh lane,由 factory 装饰器 lazy mkdir、并由下面的 envFacts 同源广告;
957
+ // adb 无此约定)。这里**明知而不豁免**:host lane 的同一豁免是经真 fs canonicalize 过的(软链会被
958
+ // 解到真身),词法版给不出同等保证,所以按 fail-closed 偏置落 ask。代价有界且可见——沙箱里写
959
+ // scratchpad 会多一次询问,而不是任何功能坏掉;终局 seam(core [2751] 真 env 窄面)到货后应当把
960
+ // 这条豁免按 host 同形补回。
961
+ {
962
+ env: new LexicalPathExecutionEnv(),
963
+ ...gateCommon,
964
+ // codex 复审 B(2026-08-05,dist 亲读+实测):core 的 canonicalizeTarget 把反斜杠 UNC 形在**入口**
965
+ // 短路成 ok:true/key=原样——不经本 env,canon 便「成功」,gate 随后就咨询豁免探针;name-keyed
966
+ // 命中即 allow,「非 POSIX 绝对形一律 ask」就被豁免会话越过(相对形/盘符形没这个洞:它们的
967
+ // canon 失败先于豁免咨询)。沙箱臂因此给探针加 canonical-key 守门:只有 `/` 开头(=真经过
968
+ // LexicalPathExecutionEnv 词法归一)的 key 才享豁免,其余落回 defaultWrite 的 ask。host 臂不加
969
+ // (真 fs canonicalize 的 key 可信,[841]① 原契约不动)。
970
+ ...(gateCommon.isExempt
907
971
  ? {
908
- isExempt: async (toolName) => {
909
- const hit = await approvalExemptionStore.has(auth.sessionId, toolName);
910
- if (hit)
911
- logger.info("fs_write_gate_exempted", { toolName, sessionId: auth.sessionId });
912
- return hit;
913
- },
972
+ isExempt: (toolName, canonicalPath) => canonicalPath.startsWith("/") ? gateCommon.isExempt(toolName) : false,
914
973
  }
915
974
  : {}),
916
975
  };
917
- })()
918
- : undefined;
919
- // [1248]②/codex F2 the workflow ask leg's session-exemption probe, on EVERY lane (unlike fsWriteGate:
920
- // the workflow gate is name-keyed, not fs-adjudicated). Same store + canonical toolName key space as the
976
+ // [1248]②/codex F2 — the workflow ask leg's session-exemption probe. Name-keyed and never fs-adjudicated,
977
+ // so it was on EVERY lane from day one (the fs gate has been too since #156, by a different route: a
978
+ // lexical env rather than no gate). Same store + canonical toolName key space as the
921
979
  // fs-gate probe — one remember="session" grant serves both; without it the durable lane re-parks every
922
980
  // later run_workflow call after an operator already granted "don't ask again this session".
923
981
  const workflowGate = approvalExemptionStore && auth?.sessionId
@@ -937,16 +995,16 @@ export function createResolveSpec(ctx) {
937
995
  throw new HttpError(422, `settings are tighten-only and cannot loosen the deployment policy: ${e.message}`);
938
996
  }
939
997
  }
940
- // subagent-hands follow-on (adversarial-review finding): core does NOT propagate
941
- // handsReadOnly/shellGate to a delegated subagent (SubagentToolOptions has NO read-only knob; subagent.js's child
942
- // runTask omits it). So a read-only (permissionMode=plan) parent that mounts the Task tool would let the CHILD get
943
- // WRITABLE hands now reachable because subRunner has an executionEnvFactory (the hands fix). That defeats plan
944
- // mode's read-only contract (a within-tenant gap, not a cross-tenant breach). Until core propagates it (relayed),
945
- // strip the delegation tools when this task is hands-read-only — a read-only run delegates nothing writable.
946
- // `stripDelegationTools` (runtime-governance.ts, tested there) matches the subagent tool ("Agent", core 1.202
947
- // canonical; legacy alias "Task") + Fork in CANONICAL space — the pre-1.202 raw `!== "Task"` filter here
948
- // silently stopped matching when core renamed the tool (fail-open: a plan-mode parent could delegate a
949
- // WRITABLE-hands child).
998
+ // subagent-hands follow-on (adversarial-review finding): a read-only (permissionMode=plan) parent that mounts
999
+ // the delegation tool used to let the CHILD get WRITABLE hands reachable once subRunner grew an
1000
+ // executionEnvFactory which defeats plan mode's read-only contract. So a hands-read-only run mounts no
1001
+ // delegation tool at all.
1002
+ // 🔴 复审 2026-08-05 亲验安装包(core 5.13.x)后修口:当年那句「core 不把 handsReadOnly/shellGate 传给
1003
+ // 子任务」**已过期** —— spec 现在两者都继承(subagent.js / prepare-task.js inheritedGateForChildren,
1004
+ // 引文在 stripDelegationTools 的注释里)。本行因此是同向第二层(更严不更松),不是唯一防线;要拆得单独决定。
1005
+ // `stripDelegationTools`(runtime-governance.ts,过滤器本体在那里被测钉住)按 core 单源常量
1006
+ // `DEFAULT_SUBAGENT_TOOL_NAME`(="Agent")RAW 比对——5.0.0 RB-476 退役了折叠面,旧名 "Task"/"Fork"
1007
+ // roster 层就 miss、不再需要归一(常量引用本身即防当年 1.202 改名时裸字符串失配的那类 fail-open)
950
1008
  if (governed.handsReadOnly === true && Array.isArray(governed.tools)) {
951
1009
  governed = { ...governed, tools: stripDelegationTools(governed.tools) };
952
1010
  }
@@ -39,7 +39,7 @@
39
39
  * across a rename of this file's *contents*; a path rename (this move, 2026-07-29) does require the
40
40
  * importers to update, which was done in the same commit.
41
41
  */
42
- export { fetchEffective, fetchPrincipalCaps, ConfigCenterHttpError, fetchSkillContent, fetchPromptArtifact, fetchPromptBlob, } from "./http-client.js";
42
+ export { fetchEffective, fetchPrincipalCaps, fetchPrincipalOrgMemory, ConfigCenterHttpError, fetchSkillContent, fetchPromptArtifact, fetchPromptBlob, } from "./http-client.js";
43
43
  export { mutateInPlace, applyEffective, applyRuntimeGates, applyRuntimeHot, resolveDefaultModelName, logEffectiveDiff, runtimeHasActiveGate, } from "./apply-effective.js";
44
44
  export { restartReasons, planeHasActiveTiers, modelPlaneChanged, type RestartSlice, type RestartSliceCtx, type RestartSignal, } from "./restart-signal.js";
45
45
  export { applyCenterSkills, resolveMcpServers, mcpForScenario } from "./skills-mcp.js";
@@ -39,7 +39,7 @@
39
39
  * across a rename of this file's *contents*; a path rename (this move, 2026-07-29) does require the
40
40
  * importers to update, which was done in the same commit.
41
41
  */
42
- export { fetchEffective, fetchPrincipalCaps, ConfigCenterHttpError, fetchSkillContent, fetchPromptArtifact, fetchPromptBlob, } from "./http-client.js";
42
+ export { fetchEffective, fetchPrincipalCaps, fetchPrincipalOrgMemory, ConfigCenterHttpError, fetchSkillContent, fetchPromptArtifact, fetchPromptBlob, } from "./http-client.js";
43
43
  export { mutateInPlace, applyEffective, applyRuntimeGates, applyRuntimeHot, resolveDefaultModelName, logEffectiveDiff, runtimeHasActiveGate, } from "./apply-effective.js";
44
44
  export { restartReasons, planeHasActiveTiers, modelPlaneChanged, } from "./restart-signal.js";
45
45
  export { applyCenterSkills, resolveMcpServers, mcpForScenario } from "./skills-mcp.js";
@@ -41,6 +41,25 @@ export declare function fetchPrincipalCaps(baseUrl: string, token: string, princ
41
41
  orgMemory?: unknown;
42
42
  etag?: string;
43
43
  } | null>;
44
+ /**
45
+ * design/170 件A —— 读 `?principal=` 响应里的 org 记忆解析段(org 目录腿的取数口)。
46
+ *
47
+ * 🔬 **为什么不复用 {@link fetchPrincipalCaps}**(复审车 D-2 修):C3「两张表失败域互不干扰」是在档
48
+ * 裁定,registry-core 的 `PrincipalCapsWire` 也正是为此把 org 段留成 `unknown`(「在本 schema 里校验
49
+ * 会把 org 段坏形连坐进 caps 腿」)。但隔离此前只做了**单向**:org 段坏不连坐 caps,caps 段坏却整体
50
+ * 连坐 org —— 治理面 `runtimeCaps`/`budget` 的一次值漂移会让整封 safeParse 失败、函数 throw,于是一个
51
+ * **形状完全正确**的 org 授权段读不出来,目录记 `fetch_failed`、准入铸瞬时拒。治理面的 fail-closed
52
+ * 极性是对的(它管的是「别静默放松治理」),但它不该决定 org 授权面的可用性。
53
+ *
54
+ * 所以这条腿用 {@link PrincipalOrgMemoryEnvelope} 只读它真正消费的两个字段:治理面漂移在这里是被
55
+ * `.passthrough()` 原样放过的未知键,不构成本腿的故障。段本身的形状校验仍在目录腿
56
+ * (`PrincipalOrgMemoryWire`),本腿保持薄透传不预判。
57
+ *
58
+ * 返回值三值语义:段键缺席 ⇒ `undefined`(旧 center 能力握手 ⇒ 目录判 `section_absent` 瞬时);
59
+ * 段在场 ⇒ 原值(空表 = 取数成功的负结果,与缺席不同格)。条件请求不适用(本腿恒不带 etag),
60
+ * 因此 304 在传输层就是 fail-loud。
61
+ */
62
+ export declare function fetchPrincipalOrgMemory(baseUrl: string, token: string, principal: string, fetchImpl?: typeof fetch, worker?: string): Promise<unknown>;
44
63
  /** An HTTP error from a config-center fetch that carries the response status so callers can branch on it
45
64
  * (e.g. translate a 404 to "unknown hash → undefined") WITHOUT parsing the message string. */
46
65
  export declare class ConfigCenterHttpError extends Error {
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * config-center HTTP client — the transport calls the service makes against the config-center
3
- * (`fetchEffective`/`fetchPrincipalCaps`/`fetchSkillContent`/`fetchPromptArtifact`/`fetchPromptBlob`) plus
4
- * the shared HTTP-error type. Split out of `facade.ts` (design/158 A13, internal-lossless — the
3
+ * (`fetchEffective`/`fetchPrincipalCaps`/`fetchPrincipalOrgMemory`/`fetchSkillContent`/`fetchPromptArtifact`/
4
+ * `fetchPromptBlob`) plus the shared HTTP-error type. 后两条 `?principal=` 腿共用 `requestPrincipalView`
5
+ * 传输半场与 `assertPrincipalEcho` 回声核,只在**解析投影**上分家(见 `fetchPrincipalOrgMemory` 头注)。 Split out of `facade.ts` (design/158 A13, internal-lossless — the
5
6
  * facade re-exports every symbol below unchanged).
6
7
  */
7
8
  import { createHash } from "node:crypto";
@@ -72,44 +73,19 @@ export async function fetchEffective(baseUrl, token, etag, fetchImpl = fetch, wo
72
73
  * content-hashed (`p-<hash>`); an `if-none-match` poll is cheap. Mirrors `fetchEffective` (Bearer + 8s + scheme guard).
73
74
  */
74
75
  export async function fetchPrincipalCaps(baseUrl, token, principal, etag, fetchImpl = fetch, worker) {
75
- const scheme = new URL(baseUrl).protocol;
76
- if (scheme !== "http:" && scheme !== "https:")
77
- throw new Error(`SEMA_REGISTRY_URL must be http(s), got "${scheme}"`);
78
- // `?principal=<id>[&worker=<self>]` — worker authorizes the wpt_<self> caps-only path; harmless on a full token.
79
- const params = new URLSearchParams({ principal });
80
- if (worker)
81
- params.set("worker", worker);
82
- const url = `${baseUrl.replace(/\/+$/, "")}/api/config/effective?${params.toString()}`;
83
- const res = await fetchImpl(url, {
84
- headers: { authorization: `Bearer ${token}`, ...(etag ? { "if-none-match": etag } : {}) },
85
- signal: AbortSignal.timeout(8000),
86
- });
87
- if (res.status === 304) {
88
- // A 304 is only valid as a reply to OUR conditional. If we sent no etag and still got 304, center is
89
- // misbehaving — DON'T treat it as "unchanged" (that surfaces undefined caps = no restriction = fail OPEN).
90
- // Throw so the resolver fail-closes (review B). With an etag, 304 = genuine "unchanged" → null.
91
- if (!etag)
92
- throw new Error("config-center returned 304 to a non-conditional principal-caps request");
76
+ const raw = await requestPrincipalView(baseUrl, token, principal, etag, fetchImpl, worker);
77
+ if (raw === null)
93
78
  return null;
94
- }
95
- if (!res.ok)
96
- throw new Error(`config-center principal-caps HTTP ${res.status}`);
97
79
  // 边界读=schema(clay 宪法 [2704] §2,同源锚=registry-core PrincipalCapsWire):治理面
98
80
  // runtimeCaps/budget 严格+passthrough(值漂移整体拒 ⇒ 本函数 throw ⇒ resolver fail-close,
99
81
  // 「outage 绝不静默 GRANT」同向);[565]/[580] fail-open 面与 orgMemory(C3 失败域隔离)在
100
82
  // schema 里保持宽读,归一/校验仍在下方与目录腿(极性不因 schema 化翻转)。
101
- const parsedBody = PrincipalCapsWire.safeParse(await res.json());
83
+ const parsedBody = PrincipalCapsWire.safeParse(raw.json);
102
84
  if (!parsedBody.success) {
103
85
  throw new Error(`config-center principal-caps body failed schema validation: ${parsedBody.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ").slice(0, 600)}`);
104
86
  }
105
87
  const body = parsedBody.data;
106
- // design/170 件A C8(响应 principal 回声核,串租户防线一行):center 两条 ?principal= 腿的 body 都
107
- // 回带 principal——在场且与请求不符 = 中间层/LB 把别人的 caps 视图递了过来,弃用整响应(throw ⇒
108
- // caps 腿 fail-close、org 目录腿记 unavailable)。键缺席 = 旧 center 形,按无证据容忍(回声核只拦
109
- // 「有回声且对不上」的确定性错递,不把旧形升级成故障)。
110
- if (body.principal !== undefined && body.principal !== principal) {
111
- throw new Error(`config-center principal-caps response echoes principal ${JSON.stringify(body.principal)} but ${JSON.stringify(principal)} was requested — discarding the whole response (cross-tenant delivery defense)`);
112
- }
88
+ assertPrincipalEcho(body.principal, principal);
113
89
  return {
114
90
  runtimeCaps: body.runtimeCaps ?? null,
115
91
  configured: Boolean(body.configured),
@@ -166,9 +142,82 @@ export async function fetchPrincipalCaps(baseUrl, token, principal, etag, fetchI
166
142
  // org 目录腿据此判 section_absent 瞬时;形状校验是 org-memory-admission 模块用 PrincipalOrgMemoryWire
167
143
  // 的事,本腿保持薄透传不预判)。
168
144
  ...("orgMemory" in body ? { orgMemory: body.orgMemory } : {}),
169
- etag: res.headers.get("etag") ?? undefined,
145
+ etag: raw.etag, // 与提取共用件之前逐字等价:键恒在场,无 ETag 头时值为 undefined(消费端只读真值)
170
146
  };
171
147
  }
148
+ /** design/170 件A C8(响应 principal 回声核):center 两条 `?principal=` 腿的 body 都回带 principal——
149
+ * 在场且与请求不符 = 中间层/LB 把别的 principal 的视图递了过来,弃用整响应(throw ⇒ caps 腿
150
+ * fail-close、org 目录腿记 unavailable)。键缺席 = 旧 center 形,按无证据容忍(回声核只拦「有回声
151
+ * 且对不上」的确定性错递,不把旧形升级成故障)。两条腿共用同一道闸,不各写一遍。 */
152
+ function assertPrincipalEcho(echoed, principal) {
153
+ if (echoed !== undefined && echoed !== principal) {
154
+ throw new Error(`config-center principal-caps response echoes principal ${JSON.stringify(echoed)} but ${JSON.stringify(principal)} was requested — discarding the whole response`);
155
+ }
156
+ }
157
+ /** 两条 `?principal=` 腿共用的**传输**半场:scheme 闸 / `?principal=[&worker=]` / Bearer / 8s 超时 /
158
+ * 304 语义。逐字来自原 {@link fetchPrincipalCaps} 函数体,提成共用件是为了第二条腿(org 段读取)
159
+ * 不必抄一遍传输纪律——抄一遍就等于给同一条边界立两套会各自漂移的规矩。
160
+ * 返回 `null` = 合法的 304(仅当我们真的发了条件请求);否则是**解析前**的 body 原值。 */
161
+ async function requestPrincipalView(baseUrl, token, principal, etag, fetchImpl, worker) {
162
+ const scheme = new URL(baseUrl).protocol;
163
+ if (scheme !== "http:" && scheme !== "https:")
164
+ throw new Error(`SEMA_REGISTRY_URL must be http(s), got "${scheme}"`);
165
+ // `?principal=<id>[&worker=<self>]` — worker authorizes the wpt_<self> caps-only path; harmless on a full token.
166
+ const params = new URLSearchParams({ principal });
167
+ if (worker)
168
+ params.set("worker", worker);
169
+ const url = `${baseUrl.replace(/\/+$/, "")}/api/config/effective?${params.toString()}`;
170
+ const res = await fetchImpl(url, {
171
+ headers: { authorization: `Bearer ${token}`, ...(etag ? { "if-none-match": etag } : {}) },
172
+ signal: AbortSignal.timeout(8000),
173
+ });
174
+ if (res.status === 304) {
175
+ // A 304 is only valid as a reply to OUR conditional. If we sent no etag and still got 304, center is
176
+ // misbehaving — DON'T treat it as "unchanged" (that surfaces undefined caps = no restriction = fail OPEN).
177
+ // Throw so the resolver fail-closes (review B). With an etag, 304 = genuine "unchanged" → null.
178
+ if (!etag)
179
+ throw new Error("config-center returned 304 to a non-conditional principal-caps request");
180
+ return null;
181
+ }
182
+ if (!res.ok)
183
+ throw new Error(`config-center principal-caps HTTP ${res.status}`);
184
+ const responseEtag = res.headers.get("etag");
185
+ return { json: await res.json(), ...(responseEtag !== null ? { etag: responseEtag } : {}) };
186
+ }
187
+ /** 件A C3 失败域隔离的读取投影:org 目录腿只消费两个字段——`principal`(回声核)与 `orgMemory` 段
188
+ * 原值。用 `PrincipalCapsWire.pick()` 取子集(schema 属主仍是 registry-core,这里不复制形状定义,
189
+ * `.passthrough()` 随 `_def` 一并继承)。 */
190
+ const PrincipalOrgMemoryEnvelope = PrincipalCapsWire.pick({ principal: true, orgMemory: true });
191
+ /**
192
+ * design/170 件A —— 读 `?principal=` 响应里的 org 记忆解析段(org 目录腿的取数口)。
193
+ *
194
+ * 🔬 **为什么不复用 {@link fetchPrincipalCaps}**(复审车 D-2 修):C3「两张表失败域互不干扰」是在档
195
+ * 裁定,registry-core 的 `PrincipalCapsWire` 也正是为此把 org 段留成 `unknown`(「在本 schema 里校验
196
+ * 会把 org 段坏形连坐进 caps 腿」)。但隔离此前只做了**单向**:org 段坏不连坐 caps,caps 段坏却整体
197
+ * 连坐 org —— 治理面 `runtimeCaps`/`budget` 的一次值漂移会让整封 safeParse 失败、函数 throw,于是一个
198
+ * **形状完全正确**的 org 授权段读不出来,目录记 `fetch_failed`、准入铸瞬时拒。治理面的 fail-closed
199
+ * 极性是对的(它管的是「别静默放松治理」),但它不该决定 org 授权面的可用性。
200
+ *
201
+ * 所以这条腿用 {@link PrincipalOrgMemoryEnvelope} 只读它真正消费的两个字段:治理面漂移在这里是被
202
+ * `.passthrough()` 原样放过的未知键,不构成本腿的故障。段本身的形状校验仍在目录腿
203
+ * (`PrincipalOrgMemoryWire`),本腿保持薄透传不预判。
204
+ *
205
+ * 返回值三值语义:段键缺席 ⇒ `undefined`(旧 center 能力握手 ⇒ 目录判 `section_absent` 瞬时);
206
+ * 段在场 ⇒ 原值(空表 = 取数成功的负结果,与缺席不同格)。条件请求不适用(本腿恒不带 etag),
207
+ * 因此 304 在传输层就是 fail-loud。
208
+ */
209
+ export async function fetchPrincipalOrgMemory(baseUrl, token, principal, fetchImpl = fetch, worker) {
210
+ const raw = await requestPrincipalView(baseUrl, token, principal, undefined, fetchImpl, worker);
211
+ // etag 恒缺席 ⇒ 304 已在传输层 throw;这一支只为类型收口(不可达)。
212
+ if (raw === null)
213
+ throw new Error("config-center returned 304 to a non-conditional org-directory fetch");
214
+ const parsed = PrincipalOrgMemoryEnvelope.safeParse(raw.json);
215
+ if (!parsed.success) {
216
+ throw new Error(`config-center org-memory section failed schema validation: ${parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ").slice(0, 600)}`);
217
+ }
218
+ assertPrincipalEcho(parsed.data.principal, principal);
219
+ return parsed.data.orgMemory;
220
+ }
172
221
  /** An HTTP error from a config-center fetch that carries the response status so callers can branch on it
173
222
  * (e.g. translate a 404 to "unknown hash → undefined") WITHOUT parsing the message string. */
174
223
  export class ConfigCenterHttpError extends Error {
@@ -21,6 +21,44 @@ export interface ScopedMcpServer {
21
21
  * off (the control plane never mounts). The runner-facing facts (the runner principal, the build-host CACHE_BASE,
22
22
  * the default base ref) live here so image-api injects only VETTED, server-fixed values into the build.sh argv —
23
23
  * operator free-text never reaches the runner. */
24
+ /** #151 车3(design/172 流内审批协议)的配置面。总开关默认 **false**;四个从属旋钮只在开关为真时生效
25
+ * (语义与「谁需要 / 谁被伤 / 什么补偿」三问见 `ServiceConfigFlat.streamApproval` 的注)。 */
26
+ export interface StreamApprovalConfig {
27
+ /** 协议总开关。`STREAM_APPROVAL_ENABLED`,默认 **false**。关 ⇒ 全链逐字现行为。
28
+ * 翻真要等回决端点(车4)与对账收敛器(车5)到位——在此之前是个不完整协议(有卡无正规回决口、
29
+ * 有 PARKING 无收敛器、跨副本回决无唤醒路径)。 */
30
+ enabled: boolean;
31
+ /** 开关为真时作为 `ToolApprovalCoordinator` 的 `ttlMs`(design/172 §3.3 的「配置窗默认」)。
32
+ * `STREAM_ASK_WINDOW_MS`,默认 **60000**。`0` = 运维显式关窗 ⇒ 恒 park(§3.3 窗=0 语义)。
33
+ * 开关为假 ⇒ 本值不参与,窗保持既有 `DEFAULT_APPROVAL_TTL_MS`(5min)。 */
34
+ windowMs: number;
35
+ /** 开流重放(§5)每次最多投几张未决卡——**读面**的帽,超出只投最新的并记一次 warn,开流不失败。
36
+ * `STREAM_APPROVAL_REPLAY_MAX`,默认 **50**。 */
37
+ replayMax: number;
38
+ /** 写侧准入门(§0 X-2)的 per-task 未决 ask 上限。超限 ⇒ 在落 pending/建 timer/发帧**之前**返回
39
+ * `"unavailable"`(park 路由,**永不 deny**——过载 bypass 到 park 是 §3.3 的硬条款)。
40
+ * `STREAM_APPROVAL_ADMIT_MAX_PER_TASK`,默认 **32**。消费点在刀 3b。 */
41
+ admitMaxPerTask: number;
42
+ /** 同上,per-owner(租户)维。`STREAM_APPROVAL_ADMIT_MAX_PER_OWNER`,默认 **256**。 */
43
+ admitMaxPerOwner: number;
44
+ /** #151 车5 §5 治理旋钮(全部 `numEnvBounded` 有界解析,§9 C6)—— 对账收敛器每 tick **每段**
45
+ * (PARKING / 孤儿 STREAM_PENDING)最多处理的行数。超出的下轮接着扫(靠 `deferReconcile` 的队列
46
+ * 轮转保证「接着」是真的,不是永远同一批)。`STREAM_APPROVAL_RECONCILE_BATCH`,默认 **200**,
47
+ * 有界 [1, 10000]。 */
48
+ reconcileBatch: number;
49
+ /** 崩溃恢复扫描的**宽限**(§8 D-1.1):只有 `expiresAtMs + 本值 < now` 的孤儿 `STREAM_PENDING` 行才
50
+ * 被 reaper 代打 `expireAsk`。活属主进程里的窗到期竞争者在这个宽限内常态必胜 ⇒ reaper 退回纯崩溃
51
+ * 兜底,不与在场闭包抢(抢 = 制造挂死面)。`STREAM_APPROVAL_PENDING_GRACE_MS`,默认 **30000**,
52
+ * 有界 [0, 3600000]。 */
53
+ pendingGraceMs: number;
54
+ /** 判据 4(adhoc 腿)的窗后宽限。`STREAM_APPROVAL_ADHOC_GRACE_MS`,默认 **60000**,有界 [0, 86400000]。
55
+ * 🔴 跨旋钮不变量:必须 **< `orphanTtlMs`**(否则判据 5 的兜底会先于判据 4 触发,adhoc 腿拿到的
56
+ * 归因就成了 `orphan_ttl_exceeded`,归因诚实性失效)——boot 期校验,破坏形**拒启**。 */
57
+ adhocGraceMs: number;
58
+ /** 判据 5(遗孤最终可判)的上界,量 immutable 的 `createdAtMs`。`STREAM_APPROVAL_ORPHAN_TTL_MS`,
59
+ * 默认 **7d**,有界 [60000, 90d]。 */
60
+ orphanTtlMs: number;
61
+ }
24
62
  export interface ImageBakeConfig {
25
63
  enabled: boolean;
26
64
  /** The dedicated operator principal the bake-runner authenticates as on the internal claim/ingest routes
@@ -717,14 +755,23 @@ export interface ServiceConfigFlat {
717
755
  * recommended set (not a merge); `off` or an empty value = disabled (empty array); unset = core's recommended
718
756
  * set. Deny composes deny-wins, so a session exemption / acceptEdits auto-allow never crosses it. */
719
757
  sensitiveWritePatterns: string[];
720
- /** [1557]§四 opt-in (cli[1555]② finding, core[1556] suggested mechanism): CC manual-family permission modes
721
- * (default/auto/acceptEdits) gate every fs WRITE hand tool (Write/Edit/NotebookEdit) but never touched Bash —
722
- * a model can `echo -n '…' > file` straight past the write-approval card. `MANUAL_MODE_SHELL_GATE` opts a
723
- * deployment into ALSO tightening `TaskSpec.shellGate` on those same modes (core `bashReversibilityProbe`
724
- * under `"classify"` only asks on constructively-irreversible commands — shell redirects etc.).
758
+ /** [1557]§四 opt-in (cli[1555]② finding, core[1556] suggested mechanism): the CC manual-family permission
759
+ * modes (default/auto/acceptEdits) gate every fs WRITE hand tool (Write/Edit/NotebookEdit) but never touched
760
+ * Bash — a model can `echo -n '…' > file` straight past the write-approval card. `MANUAL_MODE_SHELL_GATE`
761
+ * opts a deployment into ALSO tightening `TaskSpec.shellGate` (core `bashReversibilityProbe` under
762
+ * `"classify"` asks on constructively-irreversible commands — shell redirects etc. — **plus** the boundary
763
+ * case in the 🔴 note below; it is not a "constructive-irreversibility only" filter).
764
+ * 🔴 **施加点 = governance 层,无条件(#153 / [2713] 分单②;搬家前的旧描述已作废)**:与 `AUTONOMY`
765
+ * 同拍在 `applyRuntimeGovernance`(runtime-governance.ts)里施加,**不看客户端 settings/permissionMode
766
+ * 的在场性、不看落在哪个模式臂、不看执行 lane**。旧家挂在 task-settings 的 default/auto/acceptEdits 三臂
767
+ * + host-lane fs-write wiring 上,等于让一个部署级旋钮的生死由客户端表态决定(print/headless 腿不 stamp
768
+ * settings ⇒ 整块跳过;bypassPermissions 臂不回 shellGate;非 host lane wiring 不建 ⇒ 沙箱 lane 全失效)。
769
+ * rank 合成:与 autonomy 派生值取大(`ask`→`"always"` 压过 `"classify"`),对 base 只升不降。
770
+ * 客户端 settings 折叠在其后、tighten-only,拿不掉这个值;SUP 路由姿态只会把它抬到 `"always"`。
725
771
  * 🔴 **语义随 core 版本演进(2026-07-31 核)**:`"classify"` 下「纯读自动放行」这句自 core 2.7.0 起
726
772
  * **不再无条件成立** —— RB-412 给 classifier 加了 root 边界扫描,core 内部自铸 boundary
727
- * (`prepare-task.ts` 的 `shellReadBoundary`:roots=任务 root+additionalRoots,cwd=活 handsCwd),
773
+ * (`prepare-task.ts` 的 `shellReadBoundary`:roots=任务 root+additionalRoots+additionalReadRoots,
774
+ * cwd=活 handsCwd),
728
775
  * **读到 roots 之外的路径同样会 ask**(server 侧零接线即得:我们只传 shellGate,边界是 core 自算的)。
729
776
  * 旧 core(<2.7.0)下仍是名字级判定=越界读放行,两者都能工作、后者更严 ⇒ floor 不因此抬。
730
777
  * Absent/unset = `undefined` = **zero behavior change** (this is a tighten-ONLY opt-in, never a
@@ -790,6 +837,28 @@ export interface ServiceConfigFlat {
790
837
  * cap → posture-gated like askQuestion (single-user turnkey → ON; multi-tenant opt-in);
791
838
  * `TOOL_APPROVAL_ENABLED=true/false` overrides. Absent ⇒ core's headless auto-deny stands ([819]⑤ fail-closed). */
792
839
  toolApprovalEnabled: boolean;
840
+ /** #151 车2(design/172 §3.3 D3,窗长三元的安全余量):`ToolApprovalCoordinator` 的可选 `windowMarginMs`
841
+ * 构造项——有效窗 = `min(ttlMs, legRemainingMs − 本值)`,余量不足 ⇒ 不开窗直接走窗到期同路(park)。
842
+ * 仅在装配点把 `ToolApprovalRunContext.legDeadlineMonotonic` 传给协调器(车3 的活)时才实际生效——本
843
+ * 旋钮本身缺省对现行为零影响(D1)。`STREAM_ASK_WINDOW_MARGIN_MS`,默认 10000(10s)。ttl 概念沿用既有
844
+ * `approvalTimeoutSec` 族,本旋钮不新增第二个 TTL 概念。 */
845
+ streamAskWindowMarginMs: number;
846
+ /** #151 车3(design/172 流内审批协议)总开关 + 四个从属旋钮。**协议整体默认 OFF**——开关关闭时全链
847
+ * 逐字现行为:不发 `approval_request` 帧、不落 `approval_asks` 行、窗 = 既有 `DEFAULT_APPROVAL_TTL_MS`
848
+ * (5min)、`askStore` 不注入协调器。理由(三问):
849
+ * - 谁需要 60s 窗:开了协议的部署——approver 就盯着流、秒级应答,窗短 ⇒ 少占一条活腿、更快落到可恢复
850
+ * 的 park 面。这是协议的设计意图,不是随手取的数。
851
+ * - 谁被伤:①未 opt-in 的存量部署(把 5min 砍成 60s = 人离开工位 90 秒回来卡已经没了);②**park 设施
852
+ * 不在场**的部署——窗到期后 `"unavailable"` 没有降级目的地,core fail-closed deny,60s 会把
853
+ * 「人 90 秒后批准 = 放行」变成「拒绝」,是真行为回归。
854
+ * - 补偿:把 60s 绑在**新旋钮 + 新开关**上,一个字不动 `DEFAULT_APPROVAL_TTL_MS`;两边都不伤,也不给
855
+ * 既有腿新增第二个 TTL 概念。
856
+ *
857
+ * `STREAM_APPROVAL_ENABLED` / `STREAM_ASK_WINDOW_MS` / `STREAM_APPROVAL_REPLAY_MAX` /
858
+ * `STREAM_APPROVAL_ADMIT_MAX_PER_TASK` / `STREAM_APPROVAL_ADMIT_MAX_PER_OWNER` /
859
+ * `STREAM_APPROVAL_RECONCILE_BATCH` / `STREAM_APPROVAL_PENDING_GRACE_MS` /
860
+ * `STREAM_APPROVAL_ADHOC_GRACE_MS` / `STREAM_APPROVAL_ORPHAN_TTL_MS`(后四键 = 车5 收敛器)。 */
861
+ streamApproval: StreamApprovalConfig;
793
862
  /** [824]① (clay A 案 = workflow 权限全面 CC parity): workflow 子 agent 的默认权限基线与主 LLM 同权(base `{}`)。
794
863
  * 安全论证:主 LLM 与子 agent 同 root 同信任域,主 LLM 本就能写这棵树([816] ask 门照管),单独钳子 agent 的
795
864
  * 安全增益≈0(只防绕路不防直路的门不是边界);实测产品代价=[814]A 死锁。`WORKFLOW_AGENTS_READONLY=true` =
@@ -906,7 +975,7 @@ export type ServiceStoreConfig = Pick<ServiceConfigFlat, "sessionBackend" | "ses
906
975
  export type ServiceModelPlaneConfig = Pick<ServiceConfigFlat, "gatewayBaseUrl" | "gatewayApiKey" | "gatewayFallbackUrls" | "gatewayMaxRetries" | "anthropic" | "resilience" | "model" | "models" | "modelApiKeyEnv" | "modelApiKeys" | "modelQuotaWeights" | "tiers" | "projects" | "roles" | "cascadeLadder" | "degrade">;
907
976
  /** 组:approval(审批 / HITL 门)。`directDoorActive` 无 env 解析腿(装配层三域合取的产物),但语义上
908
977
  * 就是本组的门状态,故进组;`parseApprovalDomain` 的返回类型相应是 `Omit<…, "directDoorActive">`。 */
909
- export type ServiceApprovalConfig = Pick<ServiceConfigFlat, "approvalRequire" | "approvalDeny" | "approvalTimeoutSec" | "approvalAutoBudget" | "approvalNeverAuto" | "approvalHmacKeys" | "durableApproval" | "directApprovalDoor" | "directDoorActive" | "resourceSuspend" | "resourceSuspendTtlSec" | "askQuestionEnabled" | "toolApprovalEnabled" | "mcpElicitation" | "sensitiveWritePatterns" | "manualModeShellGate">;
978
+ export type ServiceApprovalConfig = Pick<ServiceConfigFlat, "approvalRequire" | "approvalDeny" | "approvalTimeoutSec" | "approvalAutoBudget" | "approvalNeverAuto" | "approvalHmacKeys" | "durableApproval" | "directApprovalDoor" | "directDoorActive" | "resourceSuspend" | "resourceSuspendTtlSec" | "askQuestionEnabled" | "toolApprovalEnabled" | "streamAskWindowMarginMs" | "streamApproval" | "mcpElicitation" | "sensitiveWritePatterns" | "manualModeShellGate">;
910
979
  /** 组:memory(记忆面 + TOC 同步腿)。 */
911
980
  export type ServiceMemoryConfig = Pick<ServiceConfigFlat, "memoryEngineEnabled" | "memoryEngineDir" | "memoryEngineRemoteLaneAllowed" | "memoryEngineBackend" | "memoryScope" | "memorySync" | "memoryOrgAdmissionMode" | "memoryOrgDirectoryJson" | "memoryOrgGrantTtlMs" | "memoryOrgUnavailableBackoffMs" | "projectMemoryEnabled" | "syncImportLeaseStaleSec">;
912
981
  /** 组:auth(鉴权 / 身份 / 治理棒)。`commandPolicy` 只有 sema-registry 腿(无 env 标量形),故 env 解析
package/dist/config.d.ts CHANGED
@@ -60,6 +60,7 @@ export declare function drainConfigNotices(): Array<{
60
60
  * (run-local historically did exactly that), so the drain lives beside the collectors, not in one entrypoint. */
61
61
  export declare function logConfigDiagnostics(logger: {
62
62
  warn: (event: string, fields?: Record<string, unknown>) => void;
63
+ info?: (event: string, fields?: Record<string, unknown>) => void;
63
64
  debug?: (event: string, fields?: Record<string, unknown>) => void;
64
65
  }): void;
65
66
  /** design/158 N8/N9 — the BOOLEAN member of this module's env-parser family (`env`/`env2`/`numEnv`/