@sema-agent/server 7.120.0 → 7.122.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 (149) hide show
  1. package/MIGRATION.md +173 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +3 -1
  4. package/USAGE.md +97 -29
  5. package/deploy/sema-up/chart/templates/server-v4pro.yaml +2 -2
  6. package/deploy/sema-up/chart/templates/server.yaml +2 -2
  7. package/deploy/sema-up/stack/sema-stack-cluster.yaml +1 -1
  8. package/dist/approval-reconciler.d.ts +8 -0
  9. package/dist/approval-reconciler.js +72 -52
  10. package/dist/auto-mode-face.d.ts +9 -2
  11. package/dist/auto-mode-face.js +5 -3
  12. package/dist/bench/s1/live-deps.js +1 -0
  13. package/dist/boot/config-center.d.ts +5 -7
  14. package/dist/boot/config-center.js +33 -9
  15. package/dist/boot/engine-lease.d.ts +1 -1
  16. package/dist/boot/leader.js +1 -0
  17. package/dist/boot/reapers.d.ts +3 -0
  18. package/dist/boot/reapers.js +2 -1
  19. package/dist/boot/resolve-spec.js +2 -4
  20. package/dist/boot/shutdown.d.ts +9 -10
  21. package/dist/boot/shutdown.js +59 -16
  22. package/dist/boot/stage-08-reapers.js +2 -2
  23. package/dist/boot/stage-09-leader.d.ts +2 -9
  24. package/dist/boot/stage-10-http-server.d.ts +1 -9
  25. package/dist/boot/stage-10-http-server.js +31 -8
  26. package/dist/capabilities/composite-members.d.ts +1 -1
  27. package/dist/capabilities/composite-members.js +1 -1
  28. package/dist/capabilities/memory-notice.d.ts +1 -1
  29. package/dist/config-catalog.js +12 -6
  30. package/dist/config-center/center-request.d.ts +13 -1
  31. package/dist/config-center/center-request.js +25 -10
  32. package/dist/config-types.d.ts +44 -5
  33. package/dist/config-types.js +6 -0
  34. package/dist/config.d.ts +35 -6
  35. package/dist/config.js +113 -33
  36. package/dist/fleet/drain-entry.d.ts +112 -0
  37. package/dist/fleet/drain-entry.js +38 -0
  38. package/dist/fleet/engine-signal.d.ts +144 -0
  39. package/dist/fleet/engine-signal.js +60 -0
  40. package/dist/fleet/serving-lease.d.ts +47 -0
  41. package/dist/fleet/serving-lease.js +233 -0
  42. package/dist/fleet-client.d.ts +35 -19
  43. package/dist/fleet-client.js +49 -25
  44. package/dist/governance-overrides.d.ts +7 -2
  45. package/dist/http/admission.js +7 -5
  46. package/dist/http/approval-sweeps.js +5 -7
  47. package/dist/http/checkpoint-ctx.d.ts +60 -11
  48. package/dist/http/checkpoint-ctx.js +23 -9
  49. package/dist/http/dispatch.js +33 -16
  50. package/dist/http/health-self-report.d.ts +60 -0
  51. package/dist/http/health-self-report.js +53 -0
  52. package/dist/http/principal-gate.d.ts +8 -1
  53. package/dist/http/principal-gate.js +1 -0
  54. package/dist/http/readiness.d.ts +25 -5
  55. package/dist/http/readiness.js +31 -6
  56. package/dist/http/resume-legs.js +9 -9
  57. package/dist/http/route-ctx.d.ts +25 -7
  58. package/dist/http/route-table.d.ts +6 -0
  59. package/dist/http/route-table.js +8 -1
  60. package/dist/http/routes/a2a-serve.d.ts +1 -1
  61. package/dist/http/routes/a2a-serve.js +5 -5
  62. package/dist/http/routes/admin-drain.d.ts +15 -31
  63. package/dist/http/routes/admin-drain.js +28 -10
  64. package/dist/http/routes/approvals-assistant.d.ts +4 -3
  65. package/dist/http/routes/approvals-assistant.js +9 -9
  66. package/dist/http/routes/capabilities.js +7 -5
  67. package/dist/http/routes/devices.js +3 -2
  68. package/dist/http/routes/fleet.d.ts +1 -0
  69. package/dist/http/routes/fleet.js +1 -1
  70. package/dist/http/routes/images.d.ts +1 -0
  71. package/dist/http/routes/images.js +1 -1
  72. package/dist/http/routes/leader.d.ts +1 -1
  73. package/dist/http/routes/leader.js +7 -1
  74. package/dist/http/routes/memory-consolidation.d.ts +1 -1
  75. package/dist/http/routes/memory-consolidation.js +1 -1
  76. package/dist/http/routes/notify-wake.d.ts +1 -1
  77. package/dist/http/routes/notify-wake.js +1 -1
  78. package/dist/http/routes/rules.js +4 -3
  79. package/dist/http/routes/runs.d.ts +4 -2
  80. package/dist/http/routes/runs.js +7 -6
  81. package/dist/http/routes/sessions.d.ts +1 -0
  82. package/dist/http/routes/sessions.js +1 -1
  83. package/dist/http/routes/side-query.d.ts +1 -1
  84. package/dist/http/routes/side-query.js +1 -1
  85. package/dist/http/routes/tasks.d.ts +2 -2
  86. package/dist/http/routes/tasks.js +3 -3
  87. package/dist/http/routes/trace-usage.d.ts +1 -0
  88. package/dist/http/routes/trace-usage.js +1 -1
  89. package/dist/http/routes/workflows.d.ts +1 -0
  90. package/dist/http/routes/workflows.js +1 -1
  91. package/dist/http/send.d.ts +13 -0
  92. package/dist/http/send.js +9 -0
  93. package/dist/http/server.d.ts +10 -21
  94. package/dist/http/server.js +20 -1
  95. package/dist/http/stored-body-replay.d.ts +27 -0
  96. package/dist/http/stored-body-replay.js +23 -0
  97. package/dist/http/wire-types.d.ts +7 -1
  98. package/dist/leader/leader.d.ts +22 -1
  99. package/dist/leader/leader.js +4 -1
  100. package/dist/leader/repair-wire.d.ts +1 -1
  101. package/dist/leader/repair-wire.js +2 -2
  102. package/dist/leader/run-entry.d.ts +27 -0
  103. package/dist/leader/run-entry.js +29 -0
  104. package/dist/leader/wire.d.ts +10 -0
  105. package/dist/leader/wire.js +15 -10
  106. package/dist/model-reach-scope.d.ts +15 -3
  107. package/dist/model-reach-scope.js +3 -2
  108. package/dist/model-route-url.d.ts +13 -20
  109. package/dist/model-route-url.js +28 -24
  110. package/dist/model-unreachable-notice.d.ts +2 -1
  111. package/dist/model-unreachable-notice.js +1 -0
  112. package/dist/observability/fail-open.d.ts +35 -2
  113. package/dist/observability/fail-open.js +20 -0
  114. package/dist/plugins/approval-ask-store-file.d.ts +1 -0
  115. package/dist/plugins/approval-ask-store-file.js +3 -0
  116. package/dist/plugins/approval-ask-store-memory.d.ts +1 -0
  117. package/dist/plugins/approval-ask-store-memory.js +6 -0
  118. package/dist/plugins/approval-ask-store-sql.d.ts +12 -0
  119. package/dist/plugins/approval-ask-store-sql.js +4 -0
  120. package/dist/plugins/one-time-migrations.js +14 -1
  121. package/dist/plugins/park-commit-watch.d.ts +19 -5
  122. package/dist/plugins/park-commit-watch.js +24 -17
  123. package/dist/plugins/permission-rule-store-sql.d.ts +2 -1
  124. package/dist/plugins/remote-env-device.js +0 -1
  125. package/dist/request-key-closure.js +1 -1
  126. package/dist/router/route-orchestration.d.ts +4 -2
  127. package/dist/router/route-orchestration.js +2 -1
  128. package/dist/rules-consent.d.ts +6 -2
  129. package/dist/rules-consent.js +3 -0
  130. package/dist/run-local.js +3 -1
  131. package/dist/runs.js +1 -1
  132. package/dist/runtime-governance.d.ts +44 -32
  133. package/dist/runtime-governance.js +23 -9
  134. package/dist/sighup-idle.d.ts +7 -1
  135. package/dist/sighup-idle.js +4 -4
  136. package/dist/task-settings.d.ts +37 -14
  137. package/dist/task-settings.js +20 -20
  138. package/dist/tool-approval.d.ts +13 -0
  139. package/dist/tool-approval.js +17 -2
  140. package/dist/trace/core-keyset-guard.d.ts +2 -2
  141. package/dist/trace/redact.d.ts +3 -3
  142. package/dist/trace/self-minted-wire.d.ts +64 -0
  143. package/dist/trace/self-minted-wire.js +30 -0
  144. package/dist/unattended-permission-mode.d.ts +17 -13
  145. package/dist/unattended-permission-mode.js +3 -1
  146. package/dist/url-shape.d.ts +31 -0
  147. package/dist/url-shape.js +41 -0
  148. package/package.json +3 -3
  149. package/scripts/drain-check-local.mjs +1 -1
@@ -2,9 +2,22 @@ import type { Logger } from "./logger.js";
2
2
  import type { Metrics } from "./metrics.js";
3
3
  /**
4
4
  * 闭集 tag 词表。形=`<repo>.<domain>.<site>`(跨仓同形,便于三仓遥测并表)。
5
- * 每项 `{ cls, note }`:`cls` 是判据表的分类(见模块头——只有这两类允许走本封装):`F`=纯体验/缓存回退,放行最坏后果只是"难看/不够好";
6
- * `P-DEBT`=保护型欠账,方向明知不对、显式记债。`note` 说清这条兜底放行了什么、最坏后果是什么(写给未来读遥测的人,不写来历)。
5
+ * 每项 `{ cls, note }`:`cls` 是判据表的分类(见模块头——只有这三类允许走本封装):`F`=纯体验/缓存回退,放行最坏后果只是"难看/不够好";
6
+ * `P-DEBT`=保护型欠账,方向明知不对、显式记债;`R`=裁定定界的放行(另带 `ruling` / `bound`)。`note` 说清这条兜底放行了什么、最坏后果是什么
7
+ * (写给未来读遥测的人,不写来历)。
7
8
  */
9
+ export type FailOpenEntry = {
10
+ readonly cls: "F";
11
+ readonly note: string;
12
+ } | {
13
+ readonly cls: "P-DEBT";
14
+ readonly note: string;
15
+ } | {
16
+ readonly cls: "R";
17
+ readonly ruling: string;
18
+ readonly bound: string;
19
+ readonly note: string;
20
+ };
8
21
  export declare const FAIL_OPEN_TAGS: {
9
22
  readonly "server.approval-ask-journal.compaction-failed": {
10
23
  readonly cls: "F";
@@ -398,6 +411,26 @@ export declare const FAIL_OPEN_TAGS: {
398
411
  readonly cls: "P-DEBT";
399
412
  readonly note: "7.120.0(`config-lkg.ts` 的 `landedPathOf`,`lkgReadDenyRows` 每次铸部署姿态座席时调;detail = `segment=<那一段的路径>`):`CONFIG_LKG_PATH` 覆写路径上有一段**目录在、却解不开**(悬空符号链接 / 链接环 / 读不了的目录:`realpath` 失败而 `lstat` 不说「不存在」)⇒ 这一段之下 LKG 真正会落在哪说不出来,读拒行只剩声明的拼写那一行(照旧在),真实落点那一行缺席。段**不存在**(`ENOENT` / `ENOTDIR`:还没建出来)是常态,不进这一臂;末段(文件本身)是悬空链接也不进(落盘的 rename 换掉的是那条目录项,文件就落在声明的拼写上)。归 P 的理由:这一行守的是 LKG 文件(含凭据),链接之后通了、LKG 写到链接目标下,模型按目标路径读得到它,直到重启重铸座席。归 DEBT 的理由:引擎没有「锚定单文件」的读拒行、座席只在 Runner 构造时铸一次,本服务说不出将来的落点;拿文件名在任意深度拒会把用户仓里同名文件一并挡掉。计数非零 = 去看 detail 点名的那一段(链接指向哪、卷挂了没有),修好后重启。";
400
413
  };
414
+ readonly "server.fleet.serving-lease-unconfirmed": {
415
+ readonly cls: "R";
416
+ readonly ruling: "C-R296";
417
+ readonly bound: "FLEET_LEASE_OUTAGE_MAX_MS";
418
+ readonly note: "`fleet/serving-lease.ts` 的 `renewOnce`(租约生效时每一次续租;detail = `worker=<w> generation=<g> why=<不可达句 | verdict=unknown>`):中心这一次**没有确认**本实例该不该服务 —— 不可达 / 超时 / 非 2xx / 答复读不成判决,或答 `unknown`(此刻没有本实例的生命周期对象)。按设计**什么都不改**(规则 2 第三臂):到期之前照常 `granted`,过期后 `expired` 照常接单,直到过期满 `FLEET_LEASE_OUTAGE_MAX_MS`(`T_max`)才拒新工作(503 `state.lease_expired`,在飞不动、永不自停)。归 R:方向与上界由裁定写定 —— 实例只在中心权威终止(`denied`)或运维覆盖时被替换,`T_max` 只是引擎自己「失联后最多再接单多久」的准入上界,不是替换依据;没确认不是被拒。放行的最坏后果 = 一只其实已被取代的实例在中心失联期间多服务至多「租约剩余 + `T_max`」(中心的代际下限持久,恢复后第一次续租即拒)。计数长期保留:持续非零 = 中心长时间没确认本实例,去看 detail 的 why(连不上 / 中心不认识这只实例)。";
419
+ };
420
+ readonly "server.fleet.serving-lease-capability-absent": {
421
+ readonly cls: "R";
422
+ readonly ruling: "C-R296";
423
+ readonly bound: "FLEET_LEASE_OUTAGE_MAX_MS";
424
+ readonly note: "`fleet/serving-lease.ts` 的 `noteCenterCapabilities`(fleet 成员每一次中心能力读数;detail = `worker=<w> generation=<g> was=<enabled|none>`):中心答复体读得出、却**没有宣告** `servingLease`(老中心 `{ ok: true }` / 降级中心 / 位撤掉)⇒ 规则 1:租约不生效,`leaseState: none`,零续租、零 503 —— 这只 fleet 成员此刻**不受围栏**。答复体读不出不进这一臂(那是「没有读数」,上一次的位与钟原样,`fleet-client.ts` `capabilityReadingOf`);已被拒的实例不进(拒是黏的,位消失不复活)。归 R:能力门控是契约规则(中心不宣告 = 中心不执行围栏),裁定写定替换只来自中心权威终止或运维覆盖;位只开关围栏、不碰钟 —— 位缺席期间 `T_max` 的钟照走(最后一次授予的到期 / 本进程第一次见到位的时刻),位一回来即按它判,所以位的抖动绕不过 `FLEET_LEASE_OUTAGE_MAX_MS`。放行的最坏后果 = 中心不执行围栏期间,一只被取代的实例照常服务(这一段由中心的选择定界,不由引擎)。计数长期保留:非零 = 这台中心没宣告租约能力。";
425
+ };
426
+ readonly "server.fleet.announce-reply-unreadable": {
427
+ readonly cls: "P-DEBT";
428
+ readonly note: "7.121.0 修复验证轮(`fleet-client.ts` 的公告环,fleet 成员每一拍心跳 2xx 答复;detail = `why=<body|not-an-object|invalid-capabilities> leaseState=<此刻的租约态>`):答复不是能力读数(体读不出 / 不是对象 / `capabilities` 键在场而形坏 —— settings-schema 9.0.0 `readCapabilityReading` 的 `unreadable`,外加本仓的 `body`)⇒ 当作没有这一拍答复,租约的状态与钟原样。**一律**计数(不论租约生效与否):租约还没生效(`leaseState=none`)时这只成员**一直不受围栏**,而修前这一形零计数(中心每拍答 `<html>` ⇒ 成员恒 none、零痕迹);租约已生效时计数说明中心的答复在坏、围栏靠的是上一次读数。归 P 类:围栏轴;归 DEBT:本进程判不出中心为什么答坏体,收口条件 = 中心契约 / 监控(中心侧对心跳答复形的执法)落地。每段连拍另有一行 warn `fleet_announce_reply_unreadable`(带 `why`)。";
429
+ };
430
+ readonly "server.fleet.denied-marker-unpersisted": {
431
+ readonly cls: "P-DEBT";
432
+ readonly note: "7.121.0(`fleet/serving-lease.ts` 的 `enterDenied`,中心以正面证据拒了本实例的服务租约那一拍;detail = `worker=<w> generation=<g>`):拒要落成数据根下一只文件(重启后起步即排空),而这一次**没落下** —— 没有数据根,或写失败(盘满 / 只读挂载 / 目录不在)。本进程照样被拒、照样当拍排空、到期自停;放行的是**重启之后**:同代际重启的实例不记得被拒,在第一次续租得到答复之前会接新工作(中心宣告了能力位时,第一次续租就再被拒一次 —— 中心的代际下限是持久的;中心那时不可达则一直接单到过期满 `FLEET_LEASE_OUTAGE_MAX_MS`)。归 P 类:这是围栏的记忆;归 DEBT:进程内无法把「记不住」变成 fail-closed(起步不接单直到第一次答复,是对租约不可达容忍规则的改动,另案)。denied 行的 `sticky` 同拍说 `process-only`,不声称跨重启记得。计数非零 = 这台机器的数据根写不进,或部署没有数据根。";
433
+ };
401
434
  };
402
435
  /** 词表键推导的闭集类型——未登记的 tag 传不进 {@link recordFailOpen}(编译期拒)。 */
403
436
  export type FailOpenTag = keyof typeof FAIL_OPEN_TAGS;
@@ -371,6 +371,26 @@ export const FAIL_OPEN_TAGS = {
371
371
  cls: "P-DEBT",
372
372
  note: "7.120.0(`config-lkg.ts` 的 `landedPathOf`,`lkgReadDenyRows` 每次铸部署姿态座席时调;detail = `segment=<那一段的路径>`):`CONFIG_LKG_PATH` 覆写路径上有一段**目录在、却解不开**(悬空符号链接 / 链接环 / 读不了的目录:`realpath` 失败而 `lstat` 不说「不存在」)⇒ 这一段之下 LKG 真正会落在哪说不出来,读拒行只剩声明的拼写那一行(照旧在),真实落点那一行缺席。段**不存在**(`ENOENT` / `ENOTDIR`:还没建出来)是常态,不进这一臂;末段(文件本身)是悬空链接也不进(落盘的 rename 换掉的是那条目录项,文件就落在声明的拼写上)。归 P 的理由:这一行守的是 LKG 文件(含凭据),链接之后通了、LKG 写到链接目标下,模型按目标路径读得到它,直到重启重铸座席。归 DEBT 的理由:引擎没有「锚定单文件」的读拒行、座席只在 Runner 构造时铸一次,本服务说不出将来的落点;拿文件名在任意深度拒会把用户仓里同名文件一并挡掉。计数非零 = 去看 detail 点名的那一段(链接指向哪、卷挂了没有),修好后重启。",
373
373
  },
374
+ "server.fleet.serving-lease-unconfirmed": {
375
+ cls: "R",
376
+ ruling: "C-R296",
377
+ bound: "FLEET_LEASE_OUTAGE_MAX_MS",
378
+ note: "`fleet/serving-lease.ts` 的 `renewOnce`(租约生效时每一次续租;detail = `worker=<w> generation=<g> why=<不可达句 | verdict=unknown>`):中心这一次**没有确认**本实例该不该服务 —— 不可达 / 超时 / 非 2xx / 答复读不成判决,或答 `unknown`(此刻没有本实例的生命周期对象)。按设计**什么都不改**(规则 2 第三臂):到期之前照常 `granted`,过期后 `expired` 照常接单,直到过期满 `FLEET_LEASE_OUTAGE_MAX_MS`(`T_max`)才拒新工作(503 `state.lease_expired`,在飞不动、永不自停)。归 R:方向与上界由裁定写定 —— 实例只在中心权威终止(`denied`)或运维覆盖时被替换,`T_max` 只是引擎自己「失联后最多再接单多久」的准入上界,不是替换依据;没确认不是被拒。放行的最坏后果 = 一只其实已被取代的实例在中心失联期间多服务至多「租约剩余 + `T_max`」(中心的代际下限持久,恢复后第一次续租即拒)。计数长期保留:持续非零 = 中心长时间没确认本实例,去看 detail 的 why(连不上 / 中心不认识这只实例)。",
379
+ },
380
+ "server.fleet.serving-lease-capability-absent": {
381
+ cls: "R",
382
+ ruling: "C-R296",
383
+ bound: "FLEET_LEASE_OUTAGE_MAX_MS",
384
+ note: "`fleet/serving-lease.ts` 的 `noteCenterCapabilities`(fleet 成员每一次中心能力读数;detail = `worker=<w> generation=<g> was=<enabled|none>`):中心答复体读得出、却**没有宣告** `servingLease`(老中心 `{ ok: true }` / 降级中心 / 位撤掉)⇒ 规则 1:租约不生效,`leaseState: none`,零续租、零 503 —— 这只 fleet 成员此刻**不受围栏**。答复体读不出不进这一臂(那是「没有读数」,上一次的位与钟原样,`fleet-client.ts` `capabilityReadingOf`);已被拒的实例不进(拒是黏的,位消失不复活)。归 R:能力门控是契约规则(中心不宣告 = 中心不执行围栏),裁定写定替换只来自中心权威终止或运维覆盖;位只开关围栏、不碰钟 —— 位缺席期间 `T_max` 的钟照走(最后一次授予的到期 / 本进程第一次见到位的时刻),位一回来即按它判,所以位的抖动绕不过 `FLEET_LEASE_OUTAGE_MAX_MS`。放行的最坏后果 = 中心不执行围栏期间,一只被取代的实例照常服务(这一段由中心的选择定界,不由引擎)。计数长期保留:非零 = 这台中心没宣告租约能力。",
385
+ },
386
+ "server.fleet.announce-reply-unreadable": {
387
+ cls: "P-DEBT",
388
+ note: "7.121.0 修复验证轮(`fleet-client.ts` 的公告环,fleet 成员每一拍心跳 2xx 答复;detail = `why=<body|not-an-object|invalid-capabilities> leaseState=<此刻的租约态>`):答复不是能力读数(体读不出 / 不是对象 / `capabilities` 键在场而形坏 —— settings-schema 9.0.0 `readCapabilityReading` 的 `unreadable`,外加本仓的 `body`)⇒ 当作没有这一拍答复,租约的状态与钟原样。**一律**计数(不论租约生效与否):租约还没生效(`leaseState=none`)时这只成员**一直不受围栏**,而修前这一形零计数(中心每拍答 `<html>` ⇒ 成员恒 none、零痕迹);租约已生效时计数说明中心的答复在坏、围栏靠的是上一次读数。归 P 类:围栏轴;归 DEBT:本进程判不出中心为什么答坏体,收口条件 = 中心契约 / 监控(中心侧对心跳答复形的执法)落地。每段连拍另有一行 warn `fleet_announce_reply_unreadable`(带 `why`)。",
389
+ },
390
+ "server.fleet.denied-marker-unpersisted": {
391
+ cls: "P-DEBT",
392
+ note: "7.121.0(`fleet/serving-lease.ts` 的 `enterDenied`,中心以正面证据拒了本实例的服务租约那一拍;detail = `worker=<w> generation=<g>`):拒要落成数据根下一只文件(重启后起步即排空),而这一次**没落下** —— 没有数据根,或写失败(盘满 / 只读挂载 / 目录不在)。本进程照样被拒、照样当拍排空、到期自停;放行的是**重启之后**:同代际重启的实例不记得被拒,在第一次续租得到答复之前会接新工作(中心宣告了能力位时,第一次续租就再被拒一次 —— 中心的代际下限是持久的;中心那时不可达则一直接单到过期满 `FLEET_LEASE_OUTAGE_MAX_MS`)。归 P 类:这是围栏的记忆;归 DEBT:进程内无法把「记不住」变成 fail-closed(起步不接单直到第一次答复,是对租约不可达容忍规则的改动,另案)。denied 行的 `sticky` 同拍说 `process-only`,不声称跨重启记得。计数非零 = 这台机器的数据根写不进,或部署没有数据根。",
393
+ },
374
394
  };
375
395
  export function failOpenTagForDroppedFrame(frameType) {
376
396
  switch (frameType) {
@@ -173,6 +173,7 @@ export declare class FileApprovalAskStore implements ApprovalAskStore {
173
173
  listPendingByTask(taskId: string, signal?: AbortSignal): Promise<AskRow[]>;
174
174
  listPendingBySession(sessionId: string, owner: string | null, signal?: AbortSignal): Promise<AskRow[]>;
175
175
  listByCall(call: AskCallCoordinate): Promise<AskRow[]>;
176
+ listParkingBySession(sessionId: string): Promise<AskRow[]>;
176
177
  getAsk(askId: string): Promise<AskRow | null>;
177
178
  getByIdempotencyKey(taskId: string, key: string): Promise<AskRow | null>;
178
179
  getBatch(batchId: string): Promise<BatchRow | null>;
@@ -256,6 +256,9 @@ export class FileApprovalAskStore {
256
256
  listByCall(call) {
257
257
  return this.core.listByCall(call);
258
258
  }
259
+ listParkingBySession(sessionId) {
260
+ return this.core.listParkingBySession(sessionId);
261
+ }
259
262
  getAsk(askId) {
260
263
  return this.core.getAsk(askId);
261
264
  }
@@ -99,6 +99,7 @@ export declare class InMemoryApprovalAskStore implements ApprovalAskStore {
99
99
  listPendingByTask(taskId: string, signal?: AbortSignal): Promise<AskRow[]>;
100
100
  listPendingBySession(sessionId: string, owner: string | null, signal?: AbortSignal): Promise<AskRow[]>;
101
101
  listByCall(call: AskCallCoordinate): Promise<AskRow[]>;
102
+ listParkingBySession(sessionId: string): Promise<AskRow[]>;
102
103
  getAsk(askId: string): Promise<AskRow | null>;
103
104
  getByIdempotencyKey(taskId: string, key: string): Promise<AskRow | null>;
104
105
  resolveProvisional(askId: string, from: AskState, to: AskState, patch: AskTransitionPatch): Promise<boolean>;
@@ -310,6 +310,12 @@ export class InMemoryApprovalAskStore {
310
310
  .sort((a, b) => a.createdAtMs - b.createdAtMs || (a.askId < b.askId ? -1 : a.askId > b.askId ? 1 : 0))
311
311
  .map((r) => ({ ...r }));
312
312
  }
313
+ async listParkingBySession(sessionId) {
314
+ return [...this.asks.values()]
315
+ .filter((r) => r.sessionId === sessionId && r.state === "PARKING")
316
+ .sort((a, b) => a.createdAtMs - b.createdAtMs || (a.askId < b.askId ? -1 : a.askId > b.askId ? 1 : 0))
317
+ .map((r) => ({ ...r }));
318
+ }
313
319
  async getAsk(askId) {
314
320
  const r = this.asks.get(askId);
315
321
  return r ? { ...r } : null;
@@ -478,6 +478,17 @@ export interface ApprovalAskStore {
478
478
  * `(task_id, state)` 的 `task_id` 前缀(一条 run 的行数 = 真出过卡的问数;grant 代答的问不落行),不另加索引。
479
479
  */
480
480
  listByCall(call: AskCallCoordinate): Promise<AskRow[]>;
481
+ /**
482
+ * 一个会话里**仍在 `PARKING`** 的全部行(序 = `created_at_ms ASC, ask_id ASC`,确定性)。
483
+ *
484
+ * 唯一消费者 = 对账收敛器的 park 提交那一拍(`approval-reconciler.ts` 的 `bindCommittedPark`):一只审批停车行提交了,收敛器要对**这次调用**
485
+ * 的 PARKING 行即时跑判据 1。停车行只陈述会话与调用 id(没有 taskId / legKey ⇒ `listByCall` 用不上);`listByState` 带 LIMIT 且按队列序,
486
+ * 新行排在队尾会被截掉。按会话读是**判据 1 能命中的行的超集**:判据 1 的候选读口按 `ask.sessionId` 查停车行,命中要求停车行的会话就是这一列。
487
+ *
488
+ * 不带租户门(内部读口,不上 wire):租户门在判据 1 的候选读口上(scope = 行的 owner)。索引走既有 `idx_ask_session_state (session_id, state)`。
489
+ * 三 twin 同答:SQL twin 读后按会话逐字再滤一遍(MySQL 协议 VARCHAR 等值的 PAD SPACE,与 `listByCall` 同一条出口对齐)。
490
+ */
491
+ listParkingBySession(sessionId: string): Promise<AskRow[]>;
481
492
  getAsk(askId: string): Promise<AskRow | null>;
482
493
  /** 回决幂等回放的读口。🔴 `taskId` **必填**且是键的第一维——UNIQUE 已改 `(task_id, idempotency_key)`
483
494
  * (车4 §12-E),裸按 key 全局查在多租户库上既可能回**别人 task** 的行(存在性外泄),也可能在同 key
@@ -636,6 +647,7 @@ export declare class SqlApprovalAskStore implements ApprovalAskStore {
636
647
  listPendingByTask(taskId: string, signal?: AbortSignal): Promise<AskRow[]>;
637
648
  listPendingBySession(sessionId: string, owner: string | null, signal?: AbortSignal): Promise<AskRow[]>;
638
649
  listByCall(call: AskCallCoordinate): Promise<AskRow[]>;
650
+ listParkingBySession(sessionId: string): Promise<AskRow[]>;
639
651
  getAsk(askId: string): Promise<AskRow | null>;
640
652
  getByIdempotencyKey(taskId: string, key: string): Promise<AskRow | null>;
641
653
  deferReconcile(askId: string, expectedState: AskState, expectedRev: number, nowMs: number): Promise<boolean>;
@@ -679,6 +679,10 @@ export class SqlApprovalAskStore {
679
679
  const { rows } = await this.db.query(this.q(`SELECT ${askCols("tidb")} FROM ${APPROVAL_ASK_TABLE} WHERE task_id = ? AND leg_key = ? AND tool_call_id = ? AND source_task_id = ? ORDER BY created_at_ms ASC, ask_id ASC`, `SELECT ${askCols("pg")} FROM ${APPROVAL_ASK_TABLE} WHERE task_id = $1 AND leg_key = $2 AND tool_call_id = $3 AND source_task_id = $4 ORDER BY created_at_ms ASC, ask_id ASC`), [call.taskId, call.legKey, call.toolCallId, call.sourceTaskId]);
680
680
  return rows.map(mapAskRow).filter((r) => askRowOnCall(r, call));
681
681
  }
682
+ async listParkingBySession(sessionId) {
683
+ const { rows } = await this.db.query(this.q(`SELECT ${askCols("tidb")} FROM ${APPROVAL_ASK_TABLE} WHERE session_id = ? AND state = 'PARKING' ORDER BY created_at_ms ASC, ask_id ASC`, `SELECT ${askCols("pg")} FROM ${APPROVAL_ASK_TABLE} WHERE session_id = $1 AND state = 'PARKING' ORDER BY created_at_ms ASC, ask_id ASC`), [sessionId]);
684
+ return rows.map(mapAskRow).filter((r) => r.sessionId === sessionId);
685
+ }
682
686
  async getAsk(askId) {
683
687
  const { rows } = await this.db.query(this.q(`SELECT ${askCols("tidb")} FROM ${APPROVAL_ASK_TABLE} WHERE ask_id = ?`, `SELECT ${askCols("pg")} FROM ${APPROVAL_ASK_TABLE} WHERE ask_id = $1`), [
684
688
  askId,
@@ -149,6 +149,19 @@ export const ONE_TIME_MIGRATIONS = [
149
149
  "出路 = 运维在库里把那只桶的那一行删掉(整桶 OCC 回写,指纹按 `buildRuleBucketChecksum` 重算)",
150
150
  reportKeys: ["undurableScopeRuleRows", "undurableScopeRuleBucketsDeferred", "undurableScopeRuleBucketsUnreadable"],
151
151
  },
152
+ {
153
+ id: "M12",
154
+ introducedIn: "7.122.0",
155
+ releasedOn: "2026-10-09",
156
+ removableAfter: "2026-11-08",
157
+ removal: "删模式词读法记号的判据与拒臂:`http/stored-body-replay.ts` 的 `permissionModeReadingUnstated`;`http/checkpoint-ctx.ts` 的 `permissionModeReadingStated` / `permissionModeReadingRefusal` / `PERMISSION_MODE_READINGS`、" +
158
+ "`StoredBodyReplayRefusal` 的 `permission_mode_reading` 臂及其日志字段与答复臂、`buildCheckpointCtx` 与 `withPermissionModeRewritten` 两处记号写点" +
159
+ "(后者删后 `resume-legs.ts` `drivePlanReview` 里对它的那一处调用回到只换体的 `{ ...ctx, body }`)、`CheckpointCtx.permissionModeReading`、" +
160
+ "`http/stored-body-replay.ts` `storedBodyReplayRefusal` 的 `autonomy` 形参(只服务这一问)与四个问点(`resume-legs.ts` 两处、`approval-sweeps.ts`、`routes/runs.ts`)传 `deps.config.autonomy` 的那一句",
161
+ afterRemoval: "没有响亮臂:删码之后仍停驻、在 7.121.x 及以前写下的行(把 `APPROVAL_TERMINAL_BACKSTOP_MS` 调长的部署,或跳过了带本条的版本直接升的库)在设了 " +
162
+ "`AUTONOMY=plan` 的部署上按体里的原词重放(运维的 plan 不再被按段重钳)—— 升级前把这些停驻的 run 决掉 / 取消,或升级窗内设 `AUTONOMY=read-only`",
163
+ reportKeys: [],
164
+ },
152
165
  ];
153
166
  export function migrationWindowOpen(window, today) {
154
167
  return window.removableAfter === null || today <= window.removableAfter;
@@ -617,7 +630,7 @@ function removedRuleRowNote(kind) {
617
630
  case "rule":
618
631
  return "a persisted permission rule whose project scope root is not a complete absolute path was removed by a one-time migration: the current engine drops such a row on every read (it protected nothing) and refuses any write naming that scope, so it could not be revoked through DELETE /v1/rules; re-add it with the project's full path if it is still wanted";
619
632
  case "revoked-rule":
620
- return "a persisted permission rule whose project scope root is not a complete absolute path, and which had already been revoked (every approval behind it is removed by a tombstone in the same bucket), was removed by a one-time migration: it granted nothing before and grants nothing now — the revocation stands, no action needed";
633
+ return "a persisted permission rule row whose project scope root is not a complete absolute path, and which had no live approval left (every approval behind it was already removed, or it never had one), was removed by a one-time migration: the row took effect nowhere before and takes effect nowhere now — no action needed";
621
634
  case "tombstone":
622
635
  return "a revocation record (tombstone) whose project scope root is not a complete absolute path was removed by a one-time migration: it can only cover rules with that same scope, which were removed with it or were never there, so nothing it revoked comes back — no action needed";
623
636
  default: {
@@ -7,7 +7,7 @@
7
7
  * 「park 落盘」,有序关停若把前者当后者,就会在 `put` 之下关掉连接池(修前病:park 行没提交,审计账本却已记「结清」)。
8
8
  *
9
9
  * 本模块给店加**一层**装饰(两店同用,不读 core 私有键 —— [ref]):
10
- * ① **计在飞 `put`**:`quiesce(deadlineMs)` 等到零或到期(绝对时刻,`Date.now()` 域),如实返回残余数 —— 收尾链在
10
+ * ① **计在飞 `put`**(连同订阅者在这一拍的工作,见下):`quiesce(deadlineMs)` 等到零或到期(绝对时刻,`Date.now()` 域),如实返回残余数 —— 收尾链在
11
11
  * `backend.close()` 之前等它(`boot/shutdown.ts`),审批协调器的关停收尾也等它(`ToolApprovalCoordinator.drainForShutdown`);
12
12
  * ② **报 park 提交** —— 「这只店里有了这一行」的两种观察,同一件事实:一次 `put` 落定(resolve);或一次 `put` 抛错之后,同一 token 的
13
13
  * `get` 读到了行(写已落、回执丢了 —— core 的挂起序正是用这一次回读确认提交的,本层观察的就是那一次读,不另读;车内 codex r1)。
@@ -15,8 +15,14 @@
15
15
  * branch on `kind` first」)⇒ 把**停车行自己陈述的**调用身份交给订阅者({@link CommittedParkCalls})。抛错且回读不到 = 没提交,不报。
16
16
  * 身份取自**读到的那一行**(不是本进程想写的那一只):哪怕 token 撞了别人的行,报出去的也只是那一行真握着的调用。
17
17
  *
18
- * 订阅者契约:**不得抛**。订阅者跑在 `put` 已提交之后、返回给 core 之前 —— 一只抛错的订阅者会把一次**已提交**的 park 报成失败。
19
- * 唯一的订阅者是审批协调器(写审计 settle 行,那只 sink 自吞失败并按 [ref] 留痕),它在自己那一侧兜住(同 tag 扩射程,见协调器)。
18
+ * 订阅者契约:**不得抛、不得拒**。订阅者跑在 `put` 已提交之后、返回给 core 之前,`put` **等它跑完**才返回 —— 一只抛错 / 拒绝的订阅者会把一次
19
+ * **已提交**的 park 报成失败;一只不收尾的订阅者会把 core 的停驻挂住(订阅者自己给自己的工作上墙钟上限)。
20
+ * 两个订阅者,各管一件「这只停车行提交了」之后的事:
21
+ * · 审批协调器 —— 有序关停下写候着的审计 settle 行(sink 自吞失败并按 [ref] 留痕,同 tag 扩射程,见协调器);
22
+ * · 对账收敛器 —— S-1280:这次调用的 `PARKING` 行在这一拍收口成 `PARKED`(判据 1 那一只判官的即时调用,`approval-reconciler.ts`
23
+ * `bindCommittedPark`;它自带墙钟预算、失败只留痕,周期扫兜底)。`put` 等它(至多到它的预算)⇒ 健康时 core 的停驻返回之时行已收口;
24
+ * 预算到点它就交还 `put`、不再发新的店调用,已发出的那一次经 {@link ParkCommitWatch.track} 留在在飞里 ⇒ 收尾链的 `quiesce`
25
+ * 在 `backend.close()` 之前等到它落定。
20
26
  */
21
27
  import type { CheckpointStore } from "@sema-agent/core";
22
28
  /**
@@ -33,9 +39,17 @@ export interface CommittedParkCalls {
33
39
  readonly toolCallIds: readonly string[];
34
40
  }
35
41
  /** 装饰层的观察面(`create*` 产出的活对象的一半;另一半是装饰后的店本身)。 */
42
+ /** park 提交的订阅者(见模块头「订阅者契约」:不得抛、不得拒;异步的那一只 `put` 等它跑完)。 */
43
+ export type ParkCommittedListener = (calls: CommittedParkCalls) => void | Promise<void>;
36
44
  export interface ParkCommitWatch {
37
- /** 订阅 park 提交(见模块头「订阅者契约」:不得抛)。 */
38
- onParkCommitted(listener: (calls: CommittedParkCalls) => void): void;
45
+ /** 订阅 park 提交(见模块头「订阅者契约」)。 */
46
+ onParkCommitted(listener: ParkCommittedListener): void;
47
+ /**
48
+ * 把订阅者在 `put` 交还之后**仍可能在跑**的那段工作记进在飞:`quiesce` 等它落定(成功或失败都算落定),返回同一个 promise。
49
+ * 用途(S-1280):订阅者给自己的工作上了墙钟预算,到点就把 `put` 交还 core —— 而此刻已经发出的那次店调用还没回来;不记进来,
50
+ * 收尾链会读到「静默」并在它底下关池。
51
+ */
52
+ track<T>(work: Promise<T>): Promise<T>;
39
53
  /** 等在飞 `put` 归零或到 `deadlineMs`(绝对时刻,`Date.now()` 域);返回那一刻的残余在飞数(0 = 静默了)。从不抛。 */
40
54
  quiesce(deadlineMs: number): Promise<{
41
55
  inFlight: number;
@@ -12,28 +12,16 @@ export function createParkCommitWatch(inner) {
12
12
  let inFlight = 0;
13
13
  const idleWaiters = new Set();
14
14
  const unconfirmed = new Set();
15
- const notify = (row) => {
15
+ const notify = async (row) => {
16
16
  const calls = committedParkCallsOf(row);
17
17
  if (calls !== undefined)
18
18
  for (const l of listeners)
19
- l(calls);
19
+ await l(calls);
20
20
  };
21
- const put = async (token, cp) => {
21
+ const counted = async (work) => {
22
22
  inFlight += 1;
23
23
  try {
24
- try {
25
- await inner.put(token, cp);
26
- }
27
- catch (err) {
28
- unconfirmed.add(token);
29
- if (unconfirmed.size > UNCONFIRMED_PUT_TOKENS_MAX) {
30
- const oldest = unconfirmed.values().next();
31
- if (oldest.done !== true)
32
- unconfirmed.delete(oldest.value);
33
- }
34
- throw err;
35
- }
36
- notify(cp);
24
+ return await work();
37
25
  }
38
26
  finally {
39
27
  inFlight -= 1;
@@ -42,13 +30,28 @@ export function createParkCommitWatch(inner) {
42
30
  wake();
43
31
  }
44
32
  };
33
+ const put = (token, cp) => counted(async () => {
34
+ try {
35
+ await inner.put(token, cp);
36
+ }
37
+ catch (err) {
38
+ unconfirmed.add(token);
39
+ if (unconfirmed.size > UNCONFIRMED_PUT_TOKENS_MAX) {
40
+ const oldest = unconfirmed.values().next();
41
+ if (oldest.done !== true)
42
+ unconfirmed.delete(oldest.value);
43
+ }
44
+ throw err;
45
+ }
46
+ await notify(cp);
47
+ });
45
48
  const get = async (token) => {
46
49
  if (!unconfirmed.has(token))
47
50
  return inner.get(token);
48
51
  unconfirmed.delete(token);
49
52
  const row = await inner.get(token);
50
53
  if (row !== null)
51
- notify(row);
54
+ await counted(() => notify(row));
52
55
  return row;
53
56
  };
54
57
  const store = new Proxy(inner, {
@@ -65,6 +68,10 @@ export function createParkCommitWatch(inner) {
65
68
  onParkCommitted(listener) {
66
69
  listeners.push(listener);
67
70
  },
71
+ track(work) {
72
+ void counted(() => work).catch(() => undefined);
73
+ return work;
74
+ },
68
75
  quiesce(deadlineMs) {
69
76
  if (inFlight === 0 || Date.now() >= deadlineMs)
70
77
  return Promise.resolve({ inFlight });
@@ -417,7 +417,8 @@ export declare const RULE_REAP_BATCH = 500;
417
417
  *
418
418
  * `kind` 是闭集(启动日志按它穷尽分句,`one-time-migrations.ts` `logOneTimeMigrations`):
419
419
  * · `rule` —— 仍有活 add 的规则行(引擎读侧每跑一次丢它并告知;它从未生效,要的话按项目的完整路径重写);
420
- * · `revoked-rule` —— 每个 add 都已被同桶墓碑撤掉的规则行(判活用 core 的 `applyTombstones`):它什么都不授予,人的撤销照旧成立,无需处置;
420
+ * · `revoked-rule` —— 没有活 add 的规则行(判活用 core 的 `applyTombstones`:每个 add 都已被同桶墓碑撤掉,或这一行本来就没有 add):它在任何一侧都已不生效
421
+ * (allow / deny / ask 同判),无需处置;逐行 warn 的句子只说这个事实,不依赖桶里有没有墓碑、也不依赖态;
421
422
  * · `tombstone` —— 一条撤销记录。它只盖得住同 (behavior, 文本, scope) 的规则 ⇒ 那些规则的 scope 与它同样不被收,随它一起删了或本来就不在,
422
423
  * 删它不会让任何被撤销的东西回来。
423
424
  */
@@ -5,7 +5,6 @@ export const DEVICE_PROVIDER = "device";
5
5
  const ok = (value) => ({ ok: true, value });
6
6
  const STREAM_PENDING_MAX_BYTES = 2 * 1024 * 1024;
7
7
  const STREAM_PENDING_MAX_ENTRIES = 8192;
8
- const VERIFY_FIRST = "may have already executed on the target; verify its effect before re-running";
9
8
  export const DEVICE_OUTCOME_TO_FILE_ERROR = {
10
9
  aborted: "aborted",
11
10
  already_exists: "already_exists",
@@ -23,7 +23,7 @@ const TASK_BODY_DECLARED_KEYS = [
23
23
  "compaction", "compactionModel", "council", "cwd", "debate", "deferTools", "deviceId", "disableSkills", "enableFork",
24
24
  "excludeAllTools", "excludeTools", "fileHistory",
25
25
  "finalVerification", "forwardSubagentEvents", "images", "inheritEnv", "interactiveTools", "jobId", "limits", "maxCostUsd",
26
- "maxTokens", "mcpServers", "memory", "memoryCapture", "memoryWrite", "model", "objective", "oneShot", "outputRetries",
26
+ "maxTokens", "mcpServers", "memory", "memoryCapture", "memoryWrite", "model", "objective", "oneShot", "outputRetries", "outputStyle",
27
27
  "outputSchema", "permissionMode", "projectId", "promptProfile", "reasoningEffort", "requireExistingSession",
28
28
  "resilience", "restoreFiles", "resumeAt", "resumeAtMode", "retainBackgroundProcesses", "retainSubagentSessions",
29
29
  "rewindFiles", "rewindFilesTo", "sandboxImageProfile", "scenario", "scratchpadDir", "selfOrchestration",
@@ -1,4 +1,4 @@
1
- import type { Autonomy } from "../runtime-governance.js";
1
+ import { type Autonomy } from "../runtime-governance.js";
2
2
  import type { GovernanceOverrides } from "../governance-overrides.js";
3
3
  export type OrchestrationMode = "single" | "supervisor" | "team";
4
4
  /** The DETERMINISTIC signals the routing decision reads (extracted by {@link deriveRoutingSignals} from the
@@ -54,7 +54,9 @@ export declare function deriveRoutingSignals(ctx: RoutingContext): RoutingSignal
54
54
  * derivation + {@link deriveRoutingSignals} + {@link routeOrchestration} in one tested call; main.ts's resolveSpec
55
55
  * calls this). Signal mapping:
56
56
  * - `isolatedExecEnv` ⟺ a sandboxed remote-exec backend (`e2b`/`k8s` Kata) — `ssh`/`host`/`adb` are NOT isolated.
57
- * - hand tools (write_file/bash) are mounted only when an executionEnv exists AND hands aren't read-only/plan.
57
+ * - hand tools (write_file/bash) are mounted only when an executionEnv exists AND the autonomy does not clamp the hands
58
+ * read-only (read off {@link autonomyOverrides} — the one place an autonomy word becomes a hands clamp; `plan` is the
59
+ * engine's plan mode, whose hands stay mounted and are gated at call time, so it is not read-only here).
58
60
  * - `canEgress` is left false in v1 (per-tool egress detection deferred; danger derives from non-isolation).
59
61
  * - `delegatedOrLongHorizon` is false on the single-task path (the (b) value lives on the /v1/leader path).
60
62
  */
@@ -1,4 +1,5 @@
1
1
  import { COARSE_SHELL_TOOLS } from "@sema-agent/core";
2
+ import { autonomyOverrides } from "../runtime-governance.js";
2
3
  import { operatorAlwaysHumanAsk } from "../operator-ask.js";
3
4
  import { heldRuleName } from "../tool-rule-names.js";
4
5
  export function routeOrchestration(s) {
@@ -20,7 +21,7 @@ export function deriveRoutingSignals(ctx) {
20
21
  };
21
22
  }
22
23
  export function routeServiceTask(input) {
23
- const readOnlyHands = input.autonomy === "read-only" || input.autonomy === "plan";
24
+ const readOnlyHands = autonomyOverrides(input.autonomy).handsReadOnly === true;
24
25
  return routeOrchestration(deriveRoutingSignals({
25
26
  ...(input.explicitTeam ? { explicitMode: "team" } : {}),
26
27
  ...(input.autonomy ? { autonomy: input.autonomy } : {}),
@@ -510,9 +510,13 @@ export type DurableRuleScope = Exclude<RuleScope, {
510
510
  * 客户端会无限重试)、引擎读侧丢(撤销口删不动),一条规则在两侧是两种命运。存量的那种行由一次性迁移 M11 删
511
511
  * (`plugins/one-time-migrations.ts` 登记表)。 */
512
512
  export declare function parseRuleScope(text: string): DurableRuleScope | undefined;
513
- /** 引擎不收的 scope(或 cc-import 层 root)的**唯一一句**拒句:写 / 撤 / 列举过滤三个路由口、cc-import 预览的层 root、local-import 的成员拒因
514
- * `scope_unreadable` 都出站这一只(各口的码照旧各自的),不在读点各抄一份。 */
513
+ /** 引擎不收的 scope 的**唯一一句**拒句:写 / 撤 / 列举过滤三个路由口、local-import 的成员拒因 `scope_unreadable` 都出站这一只
514
+ * (各口的码照旧各自的),不在读点各抄一份。cc-import 预览的层 root 判据是同一只(`isValidDurableScope`),但那份体里没有 scope 字段,
515
+ * 拒句另由 {@link buildCcImportLayerRootRefusal} 铸。 */
515
516
  export declare const RULE_SCOPE_REFUSAL_SENTENCE = "scope must be 'global' or 'project:<root>' with an absolute root";
517
+ /** cc-import 预览里第 `layerIndex` 层的 root 不是引擎收的绝对路径时的拒句 —— 点名这份体里真有的字段与层号(体里没有 scope 字段,
518
+ * 不回 {@link RULE_SCOPE_REFUSAL_SENTENCE})。判据与上面三个入口是同一只谓词,所以「absolute」是同一个词;句子只此一处铸,路由不手拼。 */
519
+ export declare function buildCcImportLayerRootRefusal(layerIndex: number): string;
516
520
  /** 一条规则在 wire 上的行 = 一个逻辑 (rule, scope) 对([ref] v2 §6 F5)。
517
521
  * `adds` **如实上 wire**、不折叠成单值:同一条规则可以被批准过多次(不同 dot、不同来源、不同时刻),
518
522
  * 折成「一个 addedAt」会让「这条规则是我导入的还是我点过卡的」在治理面上不可分。 */
@@ -61,6 +61,9 @@ export function parseRuleScope(text) {
61
61
  return isValidDurableScope(scope) ? scope : undefined;
62
62
  }
63
63
  export const RULE_SCOPE_REFUSAL_SENTENCE = "scope must be 'global' or 'project:<root>' with an absolute root";
64
+ export function buildCcImportLayerRootRefusal(layerIndex) {
65
+ return `layers[${layerIndex}].root must be an absolute path`;
66
+ }
64
67
  export function sameWireRuleIdentity(row, identity) {
65
68
  return row.behavior === identity.behavior && row.rule === identity.rule && row.scope === identity.scope;
66
69
  }
package/dist/run-local.js CHANGED
@@ -14,7 +14,7 @@ import { withUnattendedLeg } from "./unattended-permission-mode.js";
14
14
  import { buildUnattendedPolicyRefusal } from "./tool-approval.js";
15
15
  import { createBrain } from "./brain.js";
16
16
  import { assertGuardPatternsUsable, buildOnlySensitiveBaselineWarning, buildApprovalGrantNotice, createApprovalBaselinePolicy, createDeploymentGovernanceInputs, } from "./deployment-governance.js";
17
- import { applyRuntimeGovernance } from "./runtime-governance.js";
17
+ import { applyRuntimeGovernance, autonomyModeFloor } from "./runtime-governance.js";
18
18
  import { ForkRoutingSessionStore } from "./plugins/fork-routing-session-store.js";
19
19
  import { taskWallClockSec } from "./task-workflow.js";
20
20
  import { applyCatalogToSource } from "./capabilities/center-prompts.js";
@@ -633,7 +633,9 @@ export async function runLocal(argv, deps = {}) {
633
633
  ...(maxCostUsd !== undefined ? { maxCostUsd } : {}),
634
634
  ...(maxTokens !== undefined ? { maxTokens } : {}),
635
635
  };
636
+ const entryFloor = autonomyModeFloor(config.autonomy);
636
637
  const spec = withUnattendedLeg(applyLocalGovernance({
638
+ ...(entryFloor !== undefined ? { permissionMode: entryFloor } : {}),
637
639
  objective: mention.cleanedObjective,
638
640
  sessionId,
639
641
  backgroundScope: "session",
package/dist/runs.js CHANGED
@@ -423,7 +423,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
423
423
  await append("suggestions", { suggestions: suggestions.map((s) => redactSecrets(s)) });
424
424
  }
425
425
  };
426
- await instructionLeg.run(spec.sessionId, () => withModelReachScope(spec.sessionId, () => withPrincipal(principal, () => {
426
+ await instructionLeg.run(spec.sessionId, () => withModelReachScope({ sessionId: spec.sessionId, taskId }, () => withPrincipal(principal, () => {
427
427
  const withElicit = elicitation
428
428
  ? () => elicitation.runWithContext({ taskId, owner: elicitOwner ?? owner, emit: elicitEmit, abortSignal: cancelCtrl.signal }, runBody)
429
429
  : runBody;
@@ -14,16 +14,16 @@
14
14
  * toolPolicy rule is `combinePolicies(base, override)` (deny-wins) — the command gate can only ADD denies/asks
15
15
  * on top of the baseline, never clear it.
16
16
  *
17
- * TRAP #2 — `autonomy` must EXPAND INTO the three core primitives (`handsReadOnly` / `shellGate` / a
18
- * `toolPolicy`), NOT become a parallel enum on the spec (design-research §10: a parallel `autonomy` field +
19
- * `handsReadOnly:true` would conflict). The expansion is tighten-only and routed through `tightenTaskSpec`, so
17
+ * TRAP #2 — `autonomy` must EXPAND INTO core primitives (`handsReadOnly` + `writeFace` / `permissionMode` /
18
+ * `shellGate` / a `toolPolicy`), NOT become a parallel enum on the spec (design-research §10: a parallel `autonomy`
19
+ * field + `handsReadOnly:true` would conflict). The expansion is tighten-only and routed through `tightenTaskSpec`, so
20
20
  * `auto` (the loosest mode) can never LOOSEN a stricter base — `tightenTaskSpec` THROWS instead.
21
21
  *
22
22
  * ⚠️ `commandPolicy` is a COARSE name-level filter, NOT a sandbox. `sh -c …` / `$(…)` / pipes / `env X=…` /
23
23
  * path-prefixed commands all bypass the argv[0] match (they route to `ask`, fail-closed, but are not blocked by
24
24
  * name). Real isolation is the `executionEnv` sandbox. Same caveat core documents on `createCoarseCommandNamePolicy`.
25
25
  */
26
- import { type ShellGateDoctrine, type TaskSpec, type ToolPolicy } from "@sema-agent/core";
26
+ import { type PermissionMode, type ShellGateDoctrine, type TaskSpec, type ToolPolicy } from "@sema-agent/core";
27
27
  import { GovernanceConfig } from "@sema-agent/settings-schema";
28
28
  import { type GovernanceAskMarks } from "./governance-ask-marks.js";
29
29
  import { type GovernanceOverrides } from "./governance-overrides.js";
@@ -33,26 +33,6 @@ export declare const AUTONOMY_MODES: ["read-only", "ask", "plan", "auto"];
33
33
  * 契约给 `command` 加一条 refine / 给 `decision` 加一个判词,本仓的型当场跟上(或在下面的
34
34
  * `RANK` / 穷尽 switch 上编译红),而不是靠人记得把手写联合改一遍。 */
35
35
  export type CommandRule = NonNullable<GovernanceConfig["commandPolicy"]>[number];
36
- /** True when `name` names a DELEGATION tool — the subagent tool(CC-187 canonical `"Agent"`,core
37
- * `DEFAULT_SUBAGENT_TOOL_NAME`)。5.0.0 [ref]:折叠面退役,RAW 比对 core 单源常量——旧名("Task")
38
- * 在 core roster 层响亮 miss,不再需要归一;单源常量本身就防了当年 1.202 改名时裸字符串失配的
39
- * fail-open(那个教训的解=引用常量,不是折叠)。 */
40
- export declare function isDelegationToolName(name: string): boolean;
41
- /** The hands-read-only DELEGATION STRIP (the main.ts governed-spec site, extracted here so the actual filter —
42
- * not just the name predicate — is pinned by tests): a read-only (plan-mode) run does not mount a delegation
43
- * tool at all.
44
- *
45
- * 🔴 原始理由已被上游收编(2026-08-05 亲验安装包 core 5.13.x):子任务 spec 现在**继承** `handsReadOnly`
46
- * (`dist/agents/subagent.js` 的 `...(ctx.handsReadOnly === true ? { handsReadOnly: true } : {})`,resume
47
- * resume 腿同款)与 `shellGate`(`dist/core/runner/prepare-task.js` 的 `inheritedGateForChildren`),所以
48
- * 「子任务会拿到可写的手」这个当年的缺口在 core 侧已经补上。本 strip 因此是**同向的第二层**(更严,不更松):
49
- * 它保证 read-only 一档下连委派工具面都不铸,与继承是否存在无关。拆掉它是一次行为变更(read-only 任务将
50
- * 重新能委派),需要单独决定——不要因为「上游修好了」就顺手删。
51
- *
52
- * Identity (the same array) when nothing matches, so the caller's spec-object churn stays minimal. */
53
- export declare function stripDelegationTools<T extends {
54
- name: string;
55
- }>(tools: readonly T[]): T[];
56
36
  /** The operator autonomy mode (registry `governance.autonomy`) —— 契约派生(见 {@link AUTONOMY_MODES})。 */
57
37
  export type Autonomy = NonNullable<GovernanceConfig["autonomy"]>;
58
38
  /** 导出给 `test/coarse-shell-tools-mirror.test.ts` 的同源钉用(见那里的顶注)。 */
@@ -99,20 +79,33 @@ export declare function validateCommandRules(rules: CommandRule[]): string[];
99
79
  */
100
80
  export declare function compileCommandPolicyLayers(rules: CommandRule[] | undefined): ToolPolicy[];
101
81
  /**
102
- * Expand an autonomy mode into TIGHTEN-ONLY `TaskSpec` safety overrides (the primitives — TRAP #2):
103
- * - `read-only` / `plan` → `handsReadOnly: true` **+ `writeFace: "roots"`** (read + propose, never mutate; the
104
- * §6 verifier read-only boundary). `plan` shares the SAME safety boundary as `read-only`; the "present a plan
105
- * for approval" intent is higher up (UX), not a distinct TaskSpec safety field, so both map to read-only hands.
82
+ * Expand an autonomy mode into the TIGHTEN-ONLY `TaskSpec` FENCES that are re-folded on EVERY leg (first run and every
83
+ * resume rebuild) — the primitives, TRAP #2:
84
+ * - `read-only` → `handsReadOnly: true` **+ `writeFace: "roots"`** (read, never mutate; the §6 verifier read-only
85
+ * boundary). The hands clamp travels to every delegated child by core itself (`childClampSeats` — tighten-only), so the
86
+ * delegation tool stays mounted: a read-only run can delegate, and its children are read-only too.
106
87
  * - `ask` → `shellGate: "always"` (every `Bash` command tightens to an `irreversible_ask` durable suspend —
107
88
  * the fail-closed default for an unattended deployment with no parsed classifier).
108
- * - `auto` / `undefined` → `{}` (no extra tightening; still subject to the deployment baseline + commandPolicy).
89
+ * - `plan` / `auto` / `undefined` → `{}` (nothing re-folded per leg; still subject to the deployment baseline + commandPolicy).
109
90
  *
110
- * 🔴 **为什么只读那两档同时说 `writeFace:"roots"`**([ref] 随批;不是补丁,是把这一档的表态说全):
91
+ * 🔴 **规则:按段重折的只有静态围栏,模式词不在这里。** 只读手 + roots、shell 地板没有出口 —— 每一段续跑都该在,所以每一段都折。
92
+ * `plan` 是有出口的**生命周期词**(`plan_review` 把计划带出 plan),不是围栏:它是**提交入口的模式词地板**({@link autonomyModeFloor}),
93
+ * 盖在每一个 spec 出生点一次:受理门盖进存储的提交体(`task-settings.ts` `stampEntryPermissionMode`)、run-local 盖在它自建的 spec 上、
94
+ * leader run 盖在它出生时的入口体上(`leader/run-entry.ts`;入口词是 `plan` 的 leader run 进不了那条车道 —— worker 会话没有计划审阅通道 ⇒ 受理口响亮拒)。此后一条 run 的模式词
95
+ * 只有存储体一个源,写者是每一个出生点(新的持久化写都经 `http/checkpoint-ctx.ts` `buildCheckpointCtx`)与 `plan_review` 写回。按段折一个模式词 = 第二个源:被批准的计划会在下一次挂起→续跑
96
+ * 被拉回 plan。收紧入参的型上因此没有 `permissionMode`(`governance-overrides.ts`)。(上一个发布版本里本档不碰模式词,按段折的是只读手 +
97
+ * 写面 `roots`、与 `read-only` 同形 —— 运维 plan 那时根本不进 plan 模式,见下一段。)
98
+ *
99
+ * 🔴 **为什么 `plan` 不再与 `read-only` 同形**:修前两档同映「只读手 + roots」,外加 `resolve-spec.ts` 见只读手就剥委派工具 —— 于是 `plan` 有三套
100
+ * 实现(运维 plan = 只读手、用户选 plan = 模式词 + 只读手 + 剥委派、模型自进 plan = 只有模式词),而剥委派撞上请求体的 `agents` 就是 core 的
101
+ * `config.agents.no_delegation_tool` 整轮拒。现在 plan 只有引擎一个判官,`read-only` 只是只读手这一个词。
102
+ *
103
+ * 🔴 **为什么 `read-only` 同时说 `writeFace:"roots"`**([ref] 随批;不是补丁,是把这一档的表态说全):
111
104
  * core 7.18.0 的 `resolveContainmentFace` 第 1 行把「只读(verifier)挂载 × 任务层 `writeFace:"open"`」
112
105
  * 判成**真实矛盾**并**抛** `config.write_face_readonly_conflict`(理由逐字:那种挂载根本不带写面,
113
106
  * 声明一个开放写面 = 为一次调用没有的工具表态)。而 `permissionMode:"bypassPermissions"` 自 [ref] 起
114
107
  * 会在阶段④铸出 `writeFace:"open"` —— 于是「`AUTONOMY=read-only` 的部署 × 一个 bypass 请求」这一格
115
- * 不修就是**整条 run 当场配置错误失败**。
108
+ * 不修就是**整条 run 当场配置错误失败**(`plan` 一档不需要它:提交入口把模式词钳成 `plan`,bypass 那一列根本不生效)。
116
109
  *
117
110
  * 修在这里而不是在写点加一条「若 handsReadOnly 则别写」的判据:两个字段**在这一档里是同一件事**
118
111
  * (「这条 run 没有写面」),而 `tightenTaskSpec` 本来就把 `writeFace` 当 tighten-only 安全字段收
@@ -122,6 +115,25 @@ export declare function compileCommandPolicyLayers(rules: CommandRule[] | undefi
122
115
  * 它照样把键写出来(不是「只在有 open 时才压」——那又是一条判据)。
123
116
  */
124
117
  export declare function autonomyOverrides(autonomy: Autonomy | undefined): GovernanceOverrides;
118
+ /**
119
+ * 运维 autonomy 给**提交入口**的模式词地板 —— 唯一属主(规则全文见 {@link autonomyOverrides} 顶注)。`plan` ⇒ `"plan"`:每个新提交的 run
120
+ * 以 plan 起步,请求的词按 core 的模式秩钳到它之下;其余三档与缺席 ⇒ 无地板(它们的表态全在按段的围栏里)。
121
+ * 读者:三个 spec 出生点 —— 受理门(`http/admission.ts`,经 `task-settings.ts` `stampEntryPermissionMode` 盖进存储体)、run-local 自建的 spec、
122
+ * leader run 的入口体(`leader/run-entry.ts` `leaderRunEntry`,同一只盖章;leader 的 HTTP 受理口用它判「这条车道收不收」);能力面的两位 ——
123
+ * `taskWriteFaceOpen` 的治理半({@link autonomyKeepsOpenWriteFace})与 `permissionModeAuto.armed` 的意图(`auto-mode-face.ts`),两位都经 core
124
+ * `clampPermissionMode` 读「地板把请求词钳成什么」,与盖章同一只钳;存量停驻体能否重放的第三问(`http/stored-body-replay.ts`:没有当前读法记号的行,
125
+ * 体的生效词被此刻的地板钳会变 ⇒ 拒)。
126
+ * 闭集**穷尽** switch:契约给 autonomy 加一档 ⇒ 这里编译红([ref])。
127
+ */
128
+ export declare function autonomyModeFloor(autonomy: Autonomy | undefined): PermissionMode | undefined;
129
+ /**
130
+ * 运维 autonomy 留不留得住请求层「掀开写围栏」那条声明(`permissionMode:"bypassPermissions"`,引擎模式表那一列 `writeFace:"open"`)——
131
+ * 能力面 `taskWriteFaceOpen` 的治理那一半。判据读**两只属主本身**,与一条真提交走的路同序:bypass 声明先过入口地板(core
132
+ * `clampPermissionMode`,与受理门的盖章同一只钳;地板缺席 ⇒ 原样),再交给本档的按段围栏(`tightenGoverned` → core `tightenTaskSpec`),
133
+ * 看折出来的 spec 还开不开写面 —— 显式席 `"roots"` 胜过模式列(`read-only`),入口词被钳成别的词时读那个词的模式列(`plan`)。
134
+ * 本仓不另写一张「哪几档钳写面」的表:档位怎么钳 / 怎么折由两只属主说,写面开不开由引擎的模式表说。
135
+ */
136
+ export declare function autonomyKeepsOpenWriteFace(autonomy: Autonomy | undefined): boolean;
125
137
  /**
126
138
  * [ref]:**部署级**判据 —— 本部署的治理层自己是否把 shellGate 抬到 `"always"`。
127
139
  *
@@ -207,7 +219,7 @@ export declare function governanceOriginOf(gate: {
207
219
  * Apply the operator's runtime governance (autonomy + commandPolicy + manualModeShellGate + the guard set) onto a base
208
220
  * `TaskSpec`, TIGHTEN-ONLY, in a SINGLE {@link tightenTaskSpec} call: commandPolicy compiles to a `toolPolicy`
209
221
  * override (combined onto the baseline by `tightenTaskSpec` — TRAP #1), and autonomy expands to
210
- * `handsReadOnly` / `shellGate` overrides (TRAP #2). `tightenTaskSpec` THROWS `TaskSpecTightenError` if any
222
+ * `handsReadOnly` + `writeFace` / `shellGate` overrides (TRAP #2; the plan floor is the submission entrance's, never a per-leg fold). `tightenTaskSpec` THROWS `TaskSpecTightenError` if any
211
223
  * override would LOOSEN a stricter base safety field — surfacing a misconfiguration loudly instead of silently
212
224
  * weakening the gate. Returns `base` untouched when there is nothing to apply.
213
225
  *