@sema-agent/server 7.105.0 → 7.106.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 (174) hide show
  1. package/MIGRATION.md +82 -0
  2. package/USAGE.md +134 -42
  3. package/deploy/sema-up/chart/templates/server-v4pro.yaml +2 -0
  4. package/deploy/sema-up/cluster-up.sh +15 -2
  5. package/deploy/sema-up/kube-up.sh +8 -3
  6. package/deploy/sema-up/lib/flagshape.sh +29 -0
  7. package/deploy/sema-up/lib/parse-machines.py +4 -1
  8. package/deploy/sema-up/sema-up.sh +11 -9
  9. package/deploy/sema-up/stack/sema-stack-cluster.yaml +3 -1
  10. package/dist/approval-card.d.ts +1 -1
  11. package/dist/approval.d.ts +6 -1
  12. package/dist/approval.js +4 -4
  13. package/dist/auth-bridge.d.ts +14 -0
  14. package/dist/auth-bridge.js +7 -0
  15. package/dist/auto-mode-face.d.ts +49 -40
  16. package/dist/auto-mode-face.js +6 -68
  17. package/dist/bench/s1/live-deps.d.ts +9 -2
  18. package/dist/bench/s1/live-deps.js +13 -8
  19. package/dist/boot/budget-tracing.js +3 -2
  20. package/dist/boot/config-center.d.ts +4 -0
  21. package/dist/boot/config-center.js +95 -37
  22. package/dist/boot/coordinators.d.ts +1 -0
  23. package/dist/boot/coordinators.js +3 -1
  24. package/dist/boot/device-lane.d.ts +2 -1
  25. package/dist/boot/execution-env.d.ts +11 -0
  26. package/dist/boot/execution-env.js +30 -6
  27. package/dist/boot/execution-lane.js +2 -3
  28. package/dist/boot/leader.d.ts +2 -1
  29. package/dist/boot/leader.js +2 -0
  30. package/dist/boot/parked-revive-gate.d.ts +10 -7
  31. package/dist/boot/parked-revive-gate.js +4 -11
  32. package/dist/boot/resolve-spec.d.ts +1 -4
  33. package/dist/boot/resolve-spec.js +44 -21
  34. package/dist/boot/runner-deps.d.ts +5 -8
  35. package/dist/boot/runner-deps.js +3 -9
  36. package/dist/boot/side-query-lane.d.ts +2 -2
  37. package/dist/boot/stage-03-store-backend.d.ts +1 -1
  38. package/dist/boot/stage-03-store-backend.js +2 -2
  39. package/dist/boot/stage-04-stores.d.ts +1 -1
  40. package/dist/boot/stage-05-execution-env.d.ts +2 -1
  41. package/dist/boot/stage-05-execution-env.js +2 -2
  42. package/dist/boot/stage-06-runners.d.ts +3 -2
  43. package/dist/boot/stage-06-runners.js +2 -5
  44. package/dist/boot/stage-07-capability-layer.d.ts +2 -2
  45. package/dist/boot/stage-07-capability-layer.js +1 -2
  46. package/dist/boot/stage-08-reapers.d.ts +2 -2
  47. package/dist/boot/stage-09-leader.d.ts +2 -2
  48. package/dist/boot/stage-09-leader.js +2 -2
  49. package/dist/boot/stage-10-http-server.d.ts +2 -2
  50. package/dist/boot/stage-10-http-server.js +11 -7
  51. package/dist/boot/stores.d.ts +1 -1
  52. package/dist/boot/stores.js +6 -4
  53. package/dist/brain.d.ts +24 -2
  54. package/dist/brain.js +29 -10
  55. package/dist/budget.js +3 -0
  56. package/dist/capabilities/center-wiring.d.ts +64 -0
  57. package/dist/capabilities/center-wiring.js +24 -0
  58. package/dist/capabilities/composite-members.d.ts +89 -0
  59. package/dist/capabilities/composite-members.js +20 -0
  60. package/dist/capabilities/execution-lane.d.ts +3 -0
  61. package/dist/capabilities/execution-lane.js +5 -2
  62. package/dist/config-catalog.d.ts +30 -5
  63. package/dist/config-catalog.js +75 -26
  64. package/dist/config-center/apply-effective.d.ts +1 -1
  65. package/dist/config-center/apply-effective.js +7 -8
  66. package/dist/config-center/apply-ledger.d.ts +17 -0
  67. package/dist/config-center/apply-ledger.js +5 -0
  68. package/dist/config-center/center-request.d.ts +17 -0
  69. package/dist/config-center/center-request.js +4 -0
  70. package/dist/config-center/pull-state.d.ts +29 -0
  71. package/dist/config-center/pull-state.js +2 -0
  72. package/dist/config-types.d.ts +41 -14
  73. package/dist/config-types.js +1 -0
  74. package/dist/config.d.ts +35 -0
  75. package/dist/config.js +69 -16
  76. package/dist/cross-session-settings.d.ts +1 -1
  77. package/dist/deployment-governance.d.ts +12 -1
  78. package/dist/deployment-governance.js +2 -0
  79. package/dist/execution-lane-caps.d.ts +30 -2
  80. package/dist/execution-lane-caps.js +12 -0
  81. package/dist/fleet-client.d.ts +18 -13
  82. package/dist/fleet-client.js +32 -22
  83. package/dist/fleet-lease.d.ts +72 -47
  84. package/dist/fleet-lease.js +66 -14
  85. package/dist/governance-http-status.d.ts +0 -1
  86. package/dist/governance-http-status.js +0 -1
  87. package/dist/hosted-posture.d.ts +6 -17
  88. package/dist/hosted-posture.js +11 -6
  89. package/dist/http/admission.js +14 -9
  90. package/dist/http/approval-sweeps.js +13 -1
  91. package/dist/http/dispatch.js +1 -1
  92. package/dist/http/durable-run-leg.js +2 -0
  93. package/dist/http/request-guards.d.ts +25 -0
  94. package/dist/http/request-guards.js +25 -5
  95. package/dist/http/resume-legs.js +19 -11
  96. package/dist/http/route-ctx.d.ts +9 -0
  97. package/dist/http/routes/approvals-assistant.js +0 -1
  98. package/dist/http/routes/capabilities.js +12 -11
  99. package/dist/http/routes/config-catalog.js +2 -1
  100. package/dist/http/routes/diagnostics.js +2 -0
  101. package/dist/http/routes/tasks.js +4 -1
  102. package/dist/http/server.d.ts +20 -4
  103. package/dist/http/session-inherit-env.d.ts +37 -0
  104. package/dist/http/session-inherit-env.js +6 -0
  105. package/dist/http/wire-types.d.ts +19 -28
  106. package/dist/index.d.ts +2 -1
  107. package/dist/index.js +2 -1
  108. package/dist/key-resolver.d.ts +74 -19
  109. package/dist/key-resolver.js +65 -7
  110. package/dist/leader/leader.d.ts +6 -0
  111. package/dist/leader/leader.js +2 -1
  112. package/dist/leader/repair-wire.js +2 -1
  113. package/dist/leader/wire.d.ts +9 -1
  114. package/dist/leader/wire.js +10 -6
  115. package/dist/observability/fail-open.d.ts +5 -1
  116. package/dist/observability/fail-open.js +5 -1
  117. package/dist/observability/metrics.js +2 -0
  118. package/dist/observability/permission-rule-events.d.ts +1 -1
  119. package/dist/observability/permission-rule-events.js +1 -1
  120. package/dist/observability/run-terminal.d.ts +38 -1
  121. package/dist/observability/run-terminal.js +2 -5
  122. package/dist/observability/secret-env-scrub.js +1 -1
  123. package/dist/observability/tool-trace.d.ts +2 -0
  124. package/dist/operator-ask.d.ts +29 -1
  125. package/dist/operator-ask.js +6 -0
  126. package/dist/parked-decide.d.ts +14 -1
  127. package/dist/parked-decide.js +8 -1
  128. package/dist/plugins/session-policy-store-sql.d.ts +38 -7
  129. package/dist/plugins/session-policy-store-sql.js +64 -22
  130. package/dist/plugins/sql-driver.d.ts +1 -1
  131. package/dist/plugins/store-backend.d.ts +12 -3
  132. package/dist/plugins/store-backend.js +16 -5
  133. package/dist/plugins/tidb-pool.js +1 -1
  134. package/dist/read-face-posture.d.ts +4 -4
  135. package/dist/request-key-closure.js +2 -2
  136. package/dist/request-value-closure.d.ts +17 -5
  137. package/dist/request-value-closure.js +12 -0
  138. package/dist/router/route-orchestration.d.ts +11 -0
  139. package/dist/router/route-orchestration.js +7 -6
  140. package/dist/run-local.js +4 -2
  141. package/dist/runs.d.ts +2 -1
  142. package/dist/runs.js +4 -4
  143. package/dist/runtime-caps-resolver.d.ts +4 -10
  144. package/dist/runtime-governance.d.ts +8 -11
  145. package/dist/runtime-governance.js +10 -41
  146. package/dist/sealed-key.d.ts +0 -40
  147. package/dist/sealed-key.js +0 -20
  148. package/dist/server-secret-env.d.ts +11 -12
  149. package/dist/session-policy-wire.d.ts +5 -3
  150. package/dist/session-titler.d.ts +11 -0
  151. package/dist/session-titler.js +6 -2
  152. package/dist/sql-engine-posture.d.ts +2 -2
  153. package/dist/store-posture.d.ts +53 -0
  154. package/dist/store-posture.js +32 -0
  155. package/dist/task-settings.d.ts +34 -45
  156. package/dist/task-settings.js +46 -25
  157. package/dist/tool-approval.d.ts +51 -22
  158. package/dist/tool-approval.js +44 -8
  159. package/dist/tool-declaration-http-status.d.ts +12 -0
  160. package/dist/tool-declaration-http-status.js +12 -0
  161. package/dist/trace/core-keyset-guard.d.ts +7 -7
  162. package/dist/trace/engine-notice-wire.d.ts +1 -1
  163. package/dist/trace/engine-notice-wire.js +5 -0
  164. package/dist/trace/project.d.ts +3 -2
  165. package/dist/trace/project.js +51 -3
  166. package/dist/trace/wire-projection-faces.d.ts +1 -1
  167. package/dist/trace/wire-projection-faces.js +1 -1
  168. package/dist/unattended-permission-mode.d.ts +110 -0
  169. package/dist/unattended-permission-mode.js +65 -0
  170. package/package.json +3 -3
  171. package/dist/boot/session-shell-gate-registry.d.ts +0 -39
  172. package/dist/boot/session-shell-gate-registry.js +0 -21
  173. package/dist/roster-bound-http-status.d.ts +0 -6
  174. package/dist/roster-bound-http-status.js +0 -6
@@ -284,27 +284,6 @@ export interface ToolApprovalFrame {
284
284
  * ask」,治理策略产的与普通策略产的在这一个词上同形 ⇒ 顶替不了(逐字论证见 `governance-ask-marks.ts` 顶注)。
285
285
  */
286
286
  origin?: string;
287
- /**
288
- * **[ref] C3**(core 7.14.0;**ADDITIVE**,`"tool_approval"` only)—— `origin: "rule_store_unavailable"`
289
- * 的**机制判别位**:持久规则车道这一次**读不下来的是哪一半**。core 闭二词
290
- * (`RULE_STORE_UNREADABLE_KINDS`):`"store"` = 接上的规则店读不了(读失败 / 超时);`"call"` = 店读到了,
291
- * 但**这条命令**按人的 deny/ask 行读不出来(收紧 lexer 的 `unreadable`:展开里含规则词、引号没闭、语法
292
- * 错——具体那句在 `message` 里)。`AskRequest.ruleStoreUnreadable` 逐字透传。
293
- *
294
- * 🔴 **在场 ⇔ `origin === "rule_store_unavailable"`**(core 顶注逐字)。两词对审批人是两件事:`store`
295
- * 指向「去看规则店 / 网络」,`call` 指向「这条命令的写法让规则判不了」——一个 origin 词对这两件事同形。
296
- * 缺席 = 车道把店和这条命令都读通了,**或**本部署没接规则车道;消费端禁把缺席读成对规则店健康的断言。
297
- *
298
- * 🔴 **不枚举词表**(值形是 `string` 而不是本仓自铸的联合):词表单一属主在 core
299
- * (`isRuleStoreUnreadable` 是它的成员判),抄一份会把 core 加的新词吞成缺席 —— 与 {@link origin} 逐字同规。
300
- *
301
- * 🔴 **echo-only,零消费**:策略自声明的值在 core 的**唯一**盖章点就被抹掉了,server 不重算、不再筛第二遍,
302
- * 更不许据本键自铸第二张车道资格表 —— 同 `origin` / `denialLimitFallback` 两条顶注的同一条禁令。
303
- *
304
- * ⚰️ 本键的前任 `classifierUnavailable`(S-185 / [ref])随 core 7.14.0 [ref] **整族退役**:引擎那份事实改骑
305
- * `GateDisposition.denied.cause`(`tool_end.gate` / `permissionDenied.gate`),ask 面自此没有这一位。
306
- */
307
- ruleStoreUnreadable?: string;
308
287
  /**
309
288
  * **[ref]**(core 7.19.0;**ADDITIVE**,`"tool_approval"` only)—— 这只**根外读** ask 的**清障目录**:
310
289
  * 把 `dir` 加进本会话的读目录(壳侧既有的 `/add-dir` 口),同一条调用就不会再问。
@@ -319,7 +298,7 @@ export interface ToolApprovalFrame {
319
298
  *
320
299
  * 🔴 **echo-only,零判读**:server 不从命令文本重推目录,也不复核它真不真能清掉这只 ask —— core 已经
321
300
  * 把提议的根放回读边界又走了一遍才递出来,下游再判一遍就是同一事实的第二个判官(与 `origin` /
322
- * `ruleStoreUnreadable` / `denialLimitFallback` 三条顶注的同一条禁令)。
301
+ * `denialLimitFallback` 两条顶注的同一条禁令)。
323
302
  *
324
303
  * 🔴 **只此一面**:core 的负控逐字写着「the durable park row has no twin of this seat in this version」——
325
304
  * 一只 PARKED 的根外读 ask 渲的仍是单独那张确认卡。所以 `card_json` / 耐久面**没有**这一座,本仓也不
@@ -849,6 +828,10 @@ export type UnattendedPolicyRefusal = Extract<AskOutcome, {
849
828
  * 铸:park 政策的路由答 `"unavailable"`,deny 政策的 {@link UnattendedPolicyRefusal}。没有布尔臂(S-689:裸 `false`
850
829
  * 是人的拒绝,这个口从不替人说话)。 */
851
830
  export type UnattendedAskOutcome = Extract<AskOutcome, "unavailable"> | UnattendedPolicyRefusal;
831
+ /** S-877 候裁 16:耐久会话授权店的读面(`src/plugins/approval-exemption-store.ts` 的 `has`,结构型 —— 协调器不值引店模块)。 */
832
+ export interface SessionGrantReader {
833
+ has(sessionId: string, canonicalToolName: string): Promise<boolean>;
834
+ }
852
835
  /**
853
836
  * [ref] 车3 刀 3b —— 「流内审批协议到底上不上场」的**单一谓词**(设计稿 §2.3 注入 / §2.4 能力面 /
854
837
  * §8.4 park 设施自检,三处同一份判据)。
@@ -1140,6 +1123,9 @@ export declare class ToolApprovalCoordinator {
1140
1123
  * 腿闭(`runWithContext` finally)。**纯观察面**:sink 自己吞写失败并 `recordFailOpen`,任何一次审计
1141
1124
  * 写都不得改变审批终局(店头注成文)。SQL 车道恒缺席(那里有真 ask 店)。 */
1142
1125
  private readonly askAudit;
1126
+ /** S-877 候裁 16 → A:耐久会话授权店的**读面**(`/decide` 的 `remember:"session"` 写、本席读)。在场 ⇔ 部署有 store backend
1127
+ * (装配点 `boot/coordinators.ts`,与 `/decide` 写的是**同一只**店)。读口唯一 = {@link standingGrantSettles}。 */
1128
+ private readonly sessionGrants;
1143
1129
  constructor(opts?: {
1144
1130
  ttlMs?: number;
1145
1131
  /** [ref](案A):零活流铸造的 ask 的窗(见 {@link unreachedTtlMs})。装配点在协议**上场**时才传
@@ -1167,6 +1153,8 @@ export declare class ToolApprovalCoordinator {
1167
1153
  parkTombstonePerPrincipalMax?: number;
1168
1154
  /** [ref] (c):local 车道审计 sink(见 {@link ToolApprovalCoordinator.askAudit})。 */
1169
1155
  askAudit?: ApprovalAskAuditSink;
1156
+ /** S-877 候裁 16:耐久会话授权店的读面(见 {@link ToolApprovalCoordinator.sessionGrants})。 */
1157
+ sessionGrants?: SessionGrantReader;
1170
1158
  });
1171
1159
  /**
1172
1160
  * [ref] R-13 C —— 「**无人可答**」这一类终局的**唯一**成形口(park 路由 vs deny 政策)。
@@ -1350,6 +1338,47 @@ export declare class ToolApprovalCoordinator {
1350
1338
  * 面)—— 把它混进本名单会让「同 askId 的重复本地注册」拿到裸 deny,而赢家拿的是 park 路由:同一条
1351
1339
  * 持久行、两种 live 终局。那一支改走 {@link settleSameAskIdParkRoute}。 */
1352
1340
  private settleVoidedSiblings;
1341
+ /** [ref]/[ref]:本只 ask 的治理来源标(一次 peek,非消费式)。**出处决定判定**:按 `origin` 算 broker key(与写侧同一把),
1342
+ * 投递到哪条连接不参与 —— 见 {@link AskOriginIdentity} 顶注。 */
1343
+ private governanceForcedOf;
1344
+ /**
1345
+ * 🔴 **S-877 候裁 16 → A —— 站着的答案(会话授权)的唯一兑现口**(主会话裁定 2026-09-29;core 7.34.0 wire 段逐字指定的兑现位:
1346
+ * 「a host approval seat that settles asks from a session grant reads it [`authoredBy`] to keep its hands off」)。
1347
+ *
1348
+ * **一条判据**:一只 ask 由会话授权代答 ⇔ ① 它是站着的答案退得掉的一问({@link standingAnswerMayRetire} —— 治理强制 /
1349
+ * `requiresRealApproval` / `mandated` 三位的既有排除一字不动;运维「恒问人」的卡(`APPROVAL_NEVER_AUTO` / 耐久预算耗尽)
1350
+ * 自 S-877 起盖部署出身 ⇒ core 盖 `mandated`,同落这一臂)∧ ② 有真会话 ∧ ③ 两只授权店任一命中:live `allow_session` 的
1351
+ * 进程内表(按能力桶,[ref]② 修2)或耐久 `/decide` `remember:"session"` 的豁免店(按 canonical 工具名,`/decide` 写的同一只店;
1352
+ * 读失败 ⇒ 不命中 = fail-closed,人多看一张卡而不是被静默代答)∧ ④ 行上没有与本地 peek 矛盾的出处({@link sessionGrantUncontestedByRow})。
1353
+ *
1354
+ * **两个读位、一只店、按问句的作者分**(收车档 ⑩ 16):本席兑现**引擎自装的问**(模式写门 / 形状层 —— core 7.34.0 起它们是引擎
1355
+ * 自己的层、不读任何豁免,折叠 ask 赢 ⇒ 策略层探针对它们恒无效,codex F2);**本仓自己的问**(durable 名单门、workflow 问句门、
1356
+ * acceptEdits 的 cwd 域写门)照旧在各自的层上读同一只店「不问」。为什么两处都要:子代经父约束链继承到的问到达祖先审批席时 core
1357
+ * 盖 `mandated`(`ruleClearanceOf` 的 inheritedUnresolved / ancestorResolved 门),本席依约不退役 —— 只有策略层的「不问」够得着继承链
1358
+ * (主会话裁定原想退役那三只探针,施工中被 test/parked-decide-leg.test.ts「[候裁② 修]remember 全链行为」证伪,收车档 ⑩ 16-⑤)。
1359
+ * 本席的两个入口(`askBroadcast` 开头、窗 0 / 无活流身份席 {@link boundUnattendedAsk})都只调这一只。
1360
+ */
1361
+ private standingGrantSettles;
1362
+ /** 耐久豁免店的一次读(fail-closed:读抛 **或超时** ⇒ 不命中 + warn)。只由 {@link standingGrantSettles} 调。
1363
+ * 🔴 **有界**(codex 续车轮 F2):这一读排在呈卡 / 待决登记 / 审批计时器**之前**,一次黑洞化的店读会让整只 ask 连一个兜底都没有地
1364
+ * 永久悬挂(与 R2-5 抓过的 `ensureAsk` 同病)。期限取 {@link DURABLE_CONVERGE_READ_TIMEOUT_MS}(只读复核,不是做事的调用);超时 ⇒
1365
+ * 按未命中继续既有的呈卡 / 无人可答路径 —— 方向是人多看一张卡,不是被静默代答。迟到的真答案不追认(授权是「这一问要不要问」,
1366
+ * 问已经发出去了)。 */
1367
+ private durableGrantHas;
1368
+ /**
1369
+ * 🔴 **S-877 候裁 16 —— 窗 0 席**:本席**不呈卡**,但在报「无人可答」之前先问站着的答案。装配点 = 三条腿的 `windowZero` 席
1370
+ * (sync `http/routes/tasks.ts`、bg `runs.ts`、resume `http/resume-legs.ts`)。修前三处是装配层的就地闭包 `() => unattendedAskOutcome(...)`
1371
+ * —— 不读授权,引擎那一问(模式写门 / 形状层)在窗 0 部署上 remember 恒无效。改形后:授权命中 ⇒ `true`;否则逐字走
1372
+ * {@link unattendedAskOutcome} 这**一个**政策口(五臂一处施加,[ref] R-13 C 不变)。
1373
+ * parked 赎回腿的继承条目席**不**改:那条腿上到席的问全是继承问(core 盖 `mandated`),本席依约不退役,改了也兑现不了任何东西 ——
1374
+ * 赎回腿上那类问的授权照旧由重建链里 durable 名单门的策略层探针兑现(锚 = 行的 root 会话)。复活子代**自己的**引擎问(模式写门 /
1375
+ * 形状层)则经赎回腿铸的 execute ctx `onAsk` 落本席(身份 = 行的 root 会话,S-877 四续 ⑪,`parked-decide.ts` `approverSeatFor`)。
1376
+ */
1377
+ boundUnattendedAsk: (identity: {
1378
+ owner: string | null;
1379
+ taskId: string;
1380
+ sessionId?: string;
1381
+ }) => ((req: AskRequest) => Promise<AskOutcome>);
1353
1382
  /**
1354
1383
  * 🔴 session grant 短路前的**持久出处复核**([ref] 的 codex R1-[high] 补丁;[ref] 换判据)。
1355
1384
  *
@@ -363,6 +363,7 @@ export class ToolApprovalCoordinator {
363
363
  parkTombstonePerPrincipalMax;
364
364
  parkedRedeem;
365
365
  askAudit;
366
+ sessionGrants;
366
367
  constructor(opts) {
367
368
  this.ruleConsent = opts?.ruleConsent;
368
369
  this.ruleScopeRootFor = opts?.ruleScopeRootFor;
@@ -379,6 +380,7 @@ export class ToolApprovalCoordinator {
379
380
  this.parkTombstoneTtlMs = opts?.parkTombstoneTtlMs ?? DEFAULT_PARK_TOMBSTONE_TTL_MS;
380
381
  this.parkTombstonePerPrincipalMax = opts?.parkTombstonePerPrincipalMax ?? DEFAULT_PARK_TOMBSTONE_PER_PRINCIPAL_MAX;
381
382
  this.askAudit = opts?.askAudit;
383
+ this.sessionGrants = opts?.sessionGrants;
382
384
  }
383
385
  unattendedOutcome(site) {
384
386
  let outcome;
@@ -696,6 +698,45 @@ export class ToolApprovalCoordinator {
696
698
  entry.settle(false, "expired");
697
699
  }
698
700
  }
701
+ governanceForcedOf(origin, req) {
702
+ return (this.governanceAskMarks ?? governanceAskMarksFor(this.streamKey(origin.owner, origin.sessionId, origin.taskId))).isMarked(req.toolCallId);
703
+ }
704
+ async standingGrantSettles(origin, req, governanceForced) {
705
+ if (!standingAnswerMayRetire({ governanceForced, requiresRealApproval: req.requiresRealApproval === true, mandated: req.mandated === true }))
706
+ return false;
707
+ const sessionId = origin.sessionId;
708
+ if (!sessionId)
709
+ return false;
710
+ const live = this.allowAllSessions.has(sessionAllowKey(origin.owner, sessionId, sessionAllowCategory(req.toolName)));
711
+ const durable = !live && (await this.durableGrantHas(sessionId, req.toolName));
712
+ if (!live && !durable)
713
+ return false;
714
+ if (!(await this.sessionGrantUncontestedByRow(origin, req)))
715
+ return false;
716
+ if (durable)
717
+ defaultLogger.info("approval_exempted", { sessionId, toolName: req.toolName, seat: "approval_seat" });
718
+ return true;
719
+ }
720
+ async durableGrantHas(sessionId, canonicalToolName) {
721
+ if (!this.sessionGrants)
722
+ return false;
723
+ try {
724
+ const hit = await this.withStoreDeadline(this.sessionGrants.has(sessionId, canonicalToolName), "sessionGrants.has(standing-grant)", DURABLE_CONVERGE_READ_TIMEOUT_MS);
725
+ return hit === true;
726
+ }
727
+ catch (err) {
728
+ defaultLogger.warn("session_grant_read_failed", { sessionId, toolName: canonicalToolName, error: String(err).slice(0, 200) });
729
+ return false;
730
+ }
731
+ }
732
+ boundUnattendedAsk = (identity) => {
733
+ return async (req) => {
734
+ const origin = { owner: identity.owner, taskId: identity.taskId, ...(identity.sessionId !== undefined ? { sessionId: identity.sessionId } : {}), legKey: "" };
735
+ if (await this.standingGrantSettles(origin, req, this.governanceForcedOf(origin, req)))
736
+ return true;
737
+ return this.unattendedAskOutcome({ taskId: identity.taskId, owner: identity.owner, toolName: req.toolName });
738
+ };
739
+ };
699
740
  async sessionGrantUncontestedByRow(origin, req) {
700
741
  if (!this.askStore)
701
742
  return true;
@@ -944,6 +985,9 @@ export class ToolApprovalCoordinator {
944
985
  async askBroadcast(origin, ctxs, req, signal) {
945
986
  if (signal?.aborted)
946
987
  return false;
988
+ const governanceForced = this.governanceForcedOf(origin, req);
989
+ if (await this.standingGrantSettles(origin, req, governanceForced))
990
+ return true;
947
991
  const aliveCtxs = ctxs.filter((c) => !c.abortSignal?.aborted);
948
992
  const reachability = aliveCtxs.length === 0 ? "unreached" : "reachable";
949
993
  if (reachability === "unreached") {
@@ -952,13 +996,6 @@ export class ToolApprovalCoordinator {
952
996
  }
953
997
  }
954
998
  const primary = aliveCtxs[0];
955
- const governanceForced = (this.governanceAskMarks ?? governanceAskMarksFor(this.streamKey(origin.owner, origin.sessionId, origin.taskId))).isMarked(req.toolCallId);
956
- if (standingAnswerMayRetire({ governanceForced, requiresRealApproval: req.requiresRealApproval === true, mandated: req.mandated === true }) &&
957
- origin.sessionId &&
958
- this.allowAllSessions.has(sessionAllowKey(origin.owner, origin.sessionId, sessionAllowCategory(req.toolName))) &&
959
- (await this.sessionGrantUncontestedByRow(origin, req))) {
960
- return true;
961
- }
962
999
  const releaseAdmission = this.admit(origin.taskId, origin.owner);
963
1000
  if (!releaseAdmission) {
964
1001
  defaultLogger.warn("approval admission gate: too many pending asks — routing to park", {
@@ -1009,7 +1046,6 @@ export class ToolApprovalCoordinator {
1009
1046
  ...(ruleLaneMaterial !== undefined ? { ruleOffers: ruleLaneMaterial.offers } : {}),
1010
1047
  ...((a) => (a !== undefined ? { ruleOffersAbsence: a } : {}))(readRuleOffersAbsence(req)),
1011
1048
  ...((f) => (f !== undefined ? { denialLimitFallback: f } : {}))(readDenialLimitFallback(req)),
1012
- ...(typeof req.ruleStoreUnreadable === "string" && req.ruleStoreUnreadable.length > 0 ? { ruleStoreUnreadable: req.ruleStoreUnreadable } : {}),
1013
1049
  ...((g) => (g !== undefined ? { readRootCandidate: g } : {}))(readReadRootCandidate(req)),
1014
1050
  ...((c) => (c !== undefined ? { probeCause: c } : {}))(readProbeCause(req)),
1015
1051
  ...((c) => (c !== undefined ? { ruleEvidence: c } : {}))(readRuleEvidence(req)),
@@ -0,0 +1,12 @@
1
+ export declare const TOOL_DECLARATION_HTTP_STATUS: {
2
+ readonly "config.tool_mount_denied": 400;
3
+ readonly "config.tool_roster_bound": 503;
4
+ readonly "config.tool_name_too_long": 503;
5
+ readonly "config.tool_contract_too_long": 503;
6
+ readonly "config.tool_schema_bound": 503;
7
+ readonly "config.tool_face_invalid": null;
8
+ readonly "config.tool_name_invalid": null;
9
+ readonly "config.egress_requires_write_effect": null;
10
+ readonly "config.reserved_tool_name": null;
11
+ };
12
+ //# sourceMappingURL=tool-declaration-http-status.d.ts.map
@@ -0,0 +1,12 @@
1
+ export const TOOL_DECLARATION_HTTP_STATUS = {
2
+ "config.tool_mount_denied": 400,
3
+ "config.tool_roster_bound": 503,
4
+ "config.tool_name_too_long": 503,
5
+ "config.tool_contract_too_long": 503,
6
+ "config.tool_schema_bound": 503,
7
+ "config.tool_face_invalid": null,
8
+ "config.tool_name_invalid": null,
9
+ "config.egress_requires_write_effect": null,
10
+ "config.reserved_tool_name": null,
11
+ };
12
+ //# sourceMappingURL=tool-declaration-http-status.js.map
@@ -43,8 +43,8 @@ type _GuardBgNotifReverse = AssertAllKeysHandled<Exclude<BgNotifProjected | BgNo
43
43
  type RosterProjected = "name" | "agentId" | "sessionId" | "toolUseId" | "owner" | "scope" | "sessionScoped" | "rootSessionId" | "model" | "modelFallback" | "createdAt";
44
44
  type _GuardRoster = AssertAllKeysHandled<Exclude<keyof RosterEntry, RosterProjected>>;
45
45
  type _GuardRosterReverse = AssertAllKeysHandled<Exclude<RosterProjected, keyof RosterEntry>>;
46
- type AskProjected = "toolName" | "toolCallId" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName" | "delegation" | "ruleOffers" | "ruleOffersAbsence" | "persistedRuleShadowed" | "probeCause" | "ruleEvidence" | "requiresRealApproval" | "denialLimitFallback" | "origin" | "ruleStoreUnreadable" | "readRootCandidate" | "mandated" | "mandate";
47
- type AskExcluded = "preview" | "principal" | "riskAxes" | "boundInputHash" | "isDelegatedChild" | "probeReason" | "hasBidiControls" | "previewWithheld" | "execCwd";
46
+ type AskProjected = "toolName" | "toolCallId" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName" | "delegation" | "ruleOffers" | "ruleOffersAbsence" | "persistedRuleShadowed" | "probeCause" | "ruleEvidence" | "requiresRealApproval" | "denialLimitFallback" | "origin" | "readRootCandidate" | "mandated" | "mandate";
47
+ type AskExcluded = "preview" | "principal" | "riskAxes" | "boundInputHash" | "isDelegatedChild" | "probeReason" | "hasBidiControls" | "previewWithheld" | "execCwd" | "authoredBy";
48
48
  type _GuardAsk = AssertAllKeysHandled<Exclude<keyof AskRequest, AskProjected | AskExcluded>>;
49
49
  type _GuardAskReverse = AssertAllKeysHandled<Exclude<AskProjected | AskExcluded, keyof AskRequest>>;
50
50
  type AskFrameServerMinted = "type" | "approvalId" | "argsOmitted" | "outcome" | "governanceForced" | "expiresAtMs" | "expiresInMs" | "serverNowMs" | "parked" | "inputHasBidi";
@@ -133,19 +133,19 @@ type _GuardPermissionTraceReverse = AssertAllKeysHandled<Exclude<PermissionTrace
133
133
  /** `T` 非 `never` 时收敛到 `never`(空集 ⇒ 残留 `"EMPTY"` ⇒ tsc 红)。 */
134
134
  type AssertNonEmpty<T> = [T] extends [never] ? "EMPTY" : never;
135
135
  type _GuardPermissionTraceNonEmpty = AssertAllKeysHandled<AssertNonEmpty<PermissionTraceKind>>;
136
- type SummaryProjected = "sessionId" | "scope" | "createdAt" | "gateKind" | "severity" | "spentMicroUsd" | "deadline" | "hasBidiControls" | "requiresRealApproval" | "denialLimitFallback" | "origin" | "ruleStoreUnreadable" | "mandated" | "mandate";
137
- type SummaryExcluded = "token" | "toolInput" | "toolName" | "toolCallId" | "preview" | "principal" | "sourceTaskId" | "contentKind" | "restoreMode" | "checkpointId" | "previewWithheld";
136
+ type SummaryProjected = "sessionId" | "scope" | "createdAt" | "gateKind" | "severity" | "spentMicroUsd" | "deadline" | "hasBidiControls" | "requiresRealApproval" | "denialLimitFallback" | "origin" | "mandated" | "mandate";
137
+ type SummaryExcluded = "token" | "toolInput" | "toolName" | "toolCallId" | "preview" | "principal" | "sourceTaskId" | "contentKind" | "restoreMode" | "checkpointId" | "previewWithheld" | "authoredBy";
138
138
  type _GuardInboxSummary = AssertAllKeysHandled<Exclude<keyof CheckpointSummary, SummaryProjected | SummaryExcluded>>;
139
139
  type _GuardInboxSummaryReverse = AssertAllKeysHandled<Exclude<SummaryProjected | SummaryExcluded, keyof CheckpointSummary>>;
140
140
  type ParkArmKeys = keyof Extract<PendingAction, {
141
141
  kind: "tool_approval";
142
142
  }>;
143
143
  type ParkArmQueueProjected = "toolName" | "toolCallId" | "boundInputHash" | "ruleOffers" | "hasBidiControls" | keyof import("../approval-card.js").ParkRowFacts;
144
- type ParkArmQueueExcluded = "kind" | "args" | "batchToolCallIds" | "completedCallIds" | "capabilityId" | "contractId" | "shapeDigest" | "implementationRevision" | "rosterDigest" | "family" | "pathTarget" | "preview" | "previewWithheld" | "execCwd" | "denialLimitFallback" | "ruleStoreUnreadable";
144
+ type ParkArmQueueExcluded = "kind" | "args" | "batchToolCallIds" | "completedCallIds" | "capabilityId" | "contractId" | "shapeDigest" | "implementationRevision" | "rosterDigest" | "family" | "pathTarget" | "names" | "preview" | "previewWithheld" | "execCwd" | "denialLimitFallback" | "authoredBy";
145
145
  type _GuardParkArmQueue = AssertAllKeysHandled<Exclude<ParkArmKeys, ParkArmQueueProjected | ParkArmQueueExcluded>>;
146
146
  type _GuardParkArmQueueReverse = AssertAllKeysHandled<Exclude<ParkArmQueueProjected | ParkArmQueueExcluded, ParkArmKeys>>;
147
147
  type _NoDoubleBookParkArmQueue = AssertAllKeysHandled<Extract<ParkArmQueueProjected, ParkArmQueueExcluded>>;
148
- type TaskSpecWired = "objective" | "principal" | "images" | "sessionId" | "requireExistingSession" | "oneShot" | "clientContext" | "resumeAt" | "resumeAtMode" | "model" | "compactionModel" | "thinking" | "systemPrompt" | "appendSystemPrompt" | "promptProvider" | "memory" | "suggestNextPrompts" | "restoreFiles" | "acceptPartialRestore" | "rewindFilesTo" | "tools" | "excludeTools" | "deferTools" | "toolMaterializeStrategy" | "promptProfile" | "toolPolicy" | "onAsk" | "onQuestion" | "hooks" | "durableApproval" | "resourceSuspend" | "resilience" | "finalVerification" | "checkpointStore" | "handsReadOnly" | "memoryPersistenceCapable" | "additionalDirectories" | "additionalReadDirectories" | "enablePlanMode" | "interactiveTools" | "enableFork" | "shellGate" | "writeFace" | "mcp" | "a2a" | "skills" | "backgroundScope" | "envFacts" | "limits" | "configOverrides" | "forwardSubagentEvents" | "retainSubagentSessions" | "retainBackgroundProcesses" | "outputSchema" | "outputRetries" | "compaction" | "attachments" | "selfOrchestration" | "getApiKeyAndHeaders" | "interactionPosture" | "agents" | "approverPosture" | "taskId" | "signal" | "preemptSignal" | "modelRole" | "enableBlockedReport" | "maxSuspends" | "autoModeRequested" | "skillListingBudgetFraction" | "skillListingMaxDescChars" | "excludeAllTools" | "fileHistory";
148
+ type TaskSpecWired = "objective" | "principal" | "images" | "sessionId" | "requireExistingSession" | "oneShot" | "clientContext" | "resumeAt" | "resumeAtMode" | "model" | "compactionModel" | "thinking" | "systemPrompt" | "appendSystemPrompt" | "promptProvider" | "memory" | "suggestNextPrompts" | "restoreFiles" | "acceptPartialRestore" | "rewindFilesTo" | "tools" | "excludeTools" | "deferTools" | "toolMaterializeStrategy" | "promptProfile" | "toolPolicy" | "onAsk" | "onQuestion" | "hooks" | "durableApproval" | "resourceSuspend" | "resilience" | "finalVerification" | "checkpointStore" | "handsReadOnly" | "memoryPersistenceCapable" | "additionalDirectories" | "additionalReadDirectories" | "enablePlanMode" | "interactiveTools" | "enableFork" | "shellGate" | "writeFace" | "mcp" | "a2a" | "skills" | "backgroundScope" | "envFacts" | "limits" | "configOverrides" | "forwardSubagentEvents" | "retainSubagentSessions" | "retainBackgroundProcesses" | "outputSchema" | "outputRetries" | "compaction" | "attachments" | "selfOrchestration" | "getApiKeyAndHeaders" | "interactionPosture" | "agents" | "permissionMode" | "taskId" | "signal" | "preemptSignal" | "modelRole" | "enableBlockedReport" | "maxSuspends" | "skillListingBudgetFraction" | "skillListingMaxDescChars" | "excludeAllTools" | "fileHistory";
149
149
  type TaskSpecUnwired = "actor" | "roles" | "lspManager" | "tracer" | "readFace" | "readDenyPatterns" | "interactiveQuestionFallback" | "alwaysLoadTools" | "restoreGatedTools" | "deferSelfResolve" | "basePolicyForResumeEdit" | "lspDiagnostics" | "streamingToolExecution" | "toolResultThresholdChars" | "peerPresence";
150
150
  type _GuardTaskSpec = AssertAllKeysHandled<Exclude<keyof TaskSpec, TaskSpecWired | TaskSpecUnwired>>;
151
151
  type _GuardTaskSpecReverse = AssertAllKeysHandled<Exclude<TaskSpecWired | TaskSpecUnwired, keyof TaskSpec>>;
@@ -163,7 +163,7 @@ type GateOutcomeProjected = "disposition" | "settlement" | "origin";
163
163
  type _GuardGateOutcome = AssertAllKeysHandled<Exclude<keyof GateOutcome, GateOutcomeProjected>>;
164
164
  type _GuardGateOutcomeReverse = AssertAllKeysHandled<Exclude<GateOutcomeProjected, keyof GateOutcome>>;
165
165
  type GateDispositionKeys = GateDisposition extends infer D ? (D extends object ? keyof D : never) : never;
166
- type GateDispositionProjected = "kind" | "deniedBy" | "cause" | "classifier";
166
+ type GateDispositionProjected = "kind" | "deniedBy" | "cause" | "classifier" | "released" | "mode" | "askClass" | "classes";
167
167
  type _GuardGateDisposition = AssertAllKeysHandled<Exclude<GateDispositionKeys, GateDispositionProjected>>;
168
168
  type _GuardGateDispositionReverse = AssertAllKeysHandled<Exclude<GateDispositionProjected, GateDispositionKeys>>;
169
169
  type SettlementProjected = "kind" | "who" | "when" | "note";
@@ -16,7 +16,7 @@
16
16
  * 同文件 G1 e2e 的全码扇出夹具、以及契约文档附录 D.3 那张表(含**小节标题里的码数** —— 那道门本批
17
17
  * 刚立,立完就在下一次加码时自己咬住了)。
18
18
  */
19
- export declare const ENGINE_NOTICE_WIRE_CODES: readonly ["memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "task.turn_interrupted", "steering.parked_input_blocked", "task.user_steer_undrained", "task.user_followup_undrained", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "task.halt_unconsumed", "task.late_approval", "config.durable_gate_unavailable", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "mcp.injection_dropped", "delegation.ask_unresolvable", "task.interrupt_unconsumed", "approval.read_root_granted", "approval.read_root_grant_rejected", "instructions.source_changed", "session.records_skipped"];
19
+ export declare const ENGINE_NOTICE_WIRE_CODES: readonly ["memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "task.turn_interrupted", "steering.parked_input_blocked", "task.user_steer_undrained", "task.user_followup_undrained", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "task.halt_unconsumed", "task.late_approval", "config.durable_gate_unavailable", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "mcp.injection_dropped", "delegation.ask_unresolvable", "task.interrupt_unconsumed", "approval.read_root_granted", "approval.read_root_grant_rejected", "instructions.source_changed", "session.records_skipped", "classifier.unavailable", "config.permission_mode_unavailable", "permission.ask_released_by_mode", "permission.read_deny_unapplied", "permission.rule_store_unavailable"];
20
20
  export type EngineNoticeWireCode = (typeof ENGINE_NOTICE_WIRE_CODES)[number];
21
21
  /**
22
22
  * S-690 —— **本仓自铸**的通告码与它们的受众(码 / 铸点 / 受众三者的属主都是本仓)。
@@ -27,6 +27,11 @@ export const ENGINE_NOTICE_WIRE_CODES = [
27
27
  "approval.read_root_grant_rejected",
28
28
  "instructions.source_changed",
29
29
  "session.records_skipped",
30
+ "classifier.unavailable",
31
+ "config.permission_mode_unavailable",
32
+ "permission.ask_released_by_mode",
33
+ "permission.read_deny_unapplied",
34
+ "permission.rule_store_unavailable",
30
35
  ];
31
36
  export const SERVER_NOTICE_AUDIENCE = {
32
37
  "instructions.source_changed": "user",
@@ -578,8 +578,9 @@ export declare function workspaceChangedEventData(ev: {
578
578
  * 两词:core 只看合成后的 caps deny 位,本仓分得出这个 false 是 center 下发的还是本地 env 写的)。
579
579
  * ⚠️ **`settings_denied` 不折进 `denied`,它折进 `no_intent`**(codex 车BO r1 [medium] 验真后纠):
580
580
  * settings 层的 kill-switch 折的是**模式**(`effectivePermissionMode`:auto → default),于是 resolve-spec
581
- * 阶段④ 压根不写意图座 `TaskSpec.autoModeRequested` ⇒ core 的武装谓词第一项就落空;而本地 env 腿折的是
582
- * **caps**(`applyLocalAutoModeDeny`),模式不动、座照写 ⇒ core 才报 `denied`。两条腿在 manifest 上
581
+ * 阶段④ 写的 `TaskSpec.permissionMode` 是 `"default"`(S-877 前是「不写意图座 `autoModeRequested`」)⇒ core 的武装谓词第一项就落空;
582
+ * 而本地 env 腿折的是 **caps**(`applyLocalAutoModeDeny`),模式照写 `"auto"` ⇒ core 才报 `denied`(并按 `operator_deny` 折 default、发
583
+ * `config.permission_mode_unavailable`)。两条腿在 manifest 上
583
584
  * **分不开**,要分辨得读 `/v1/capabilities`。对照表与保鲜钉在 `test/wiring-manifest-projection.test.ts`
584
585
  * 的 147③ 节,分岔的行为面格在 `test/permission-mode-shell-gate-translation.test.ts`;
585
586
  * 本函数**只透传**,一个字都不换。
@@ -1,5 +1,5 @@
1
1
  import { admitProviderEcho, redactErrorMessage, redactModelText } from "../observability/run-terminal.js";
2
- import { MCP_DELIVERY_VERDICTS, MCP_LIVENESS_STATES, screenGateOutcome, TEAM_ROLES, ToolRoster, ToolRosterDelta, WRITE_PROTECTION_TARGET_VIEWS } from "@sema-agent/core";
2
+ import { isAskClass, isAskOrigin, isPermissionMode, isPersistedRuleMandate, MCP_DELIVERY_VERDICTS, MCP_LIVENESS_STATES, screenGateOutcome, TEAM_ROLES, ToolRoster, ToolRosterDelta, WRITE_PROTECTION_TARGET_VIEWS } from "@sema-agent/core";
3
3
  import { Value } from "typebox/value";
4
4
  import { recordFailOpen } from "../observability/fail-open.js";
5
5
  import { redactSecrets, redactDeep } from "./redact.js";
@@ -103,6 +103,34 @@ export function screenedGate(v) {
103
103
  return undefined;
104
104
  }
105
105
  }
106
+ function modeLedgerDefect(d) {
107
+ const classesOk = (v) => Array.isArray(v) && v.every((c) => isAskClass(c));
108
+ if (d.kind === "denied") {
109
+ if (d.mode !== undefined && !isPermissionMode(d.mode))
110
+ return "disposition.mode is outside the closed set";
111
+ if (d.askClass !== undefined && !isAskClass(d.askClass))
112
+ return "disposition.askClass is outside the closed set";
113
+ if (d.classes !== undefined && !classesOk(d.classes))
114
+ return "disposition.classes is not a list of ask classes";
115
+ return false;
116
+ }
117
+ if (d.released === undefined)
118
+ return false;
119
+ const r = d.released;
120
+ if (r === null || typeof r !== "object")
121
+ return "disposition.released is not an object";
122
+ const o = r;
123
+ if (!isPermissionMode(o.mode) || !isAskClass(o.askClass) || !classesOk(o.classes))
124
+ return "disposition.released.mode / askClass / classes is outside its closed set";
125
+ if (o.origin !== undefined && !isAskOrigin(o.origin))
126
+ return "disposition.released.origin is outside the closed set";
127
+ if (o.mandate !== undefined && !isPersistedRuleMandate(o.mandate))
128
+ return "disposition.released.mandate is outside the closed set";
129
+ if (o.reason !== undefined && !(typeof o.reason === "string" && Object.hasOwn(RELEASE_REASON_WORDS, o.reason)))
130
+ return "disposition.released.reason is outside the closed set";
131
+ return false;
132
+ }
133
+ const RELEASE_REASON_WORDS = { rule_unreadable: true };
106
134
  function screenedGateOrThrow(v) {
107
135
  if (v === null || typeof v !== "object") {
108
136
  if (v !== undefined)
@@ -121,6 +149,7 @@ function screenedGateOrThrow(v) {
121
149
  (d.classifier !== undefined && (d.classifier === null || typeof d.classifier !== "object") && "disposition.classifier is not an object") ||
122
150
  (classifierFallbackRaw !== undefined && (classifierFallbackRaw === null || typeof classifierFallbackRaw !== "object") && "disposition.classifier.fallback is not an object") ||
123
151
  (classifierFallbackRaw !== undefined && classifierFallbackRaw !== null && typeof classifierFallbackRaw === "object" && typeof classifierFallbackRaw.cause !== "string" && "disposition.classifier.fallback.cause is not a string") ||
152
+ modeLedgerDefect(d) ||
124
153
  (g.settlement !== undefined && (g.settlement === null || typeof g.settlement !== "object") && "settlement is not an object") ||
125
154
  (g.settlement !== undefined && g.settlement !== null && typeof g.settlement.kind !== "string" && "settlement.kind is not a string") ||
126
155
  (g.origin !== undefined && typeof g.origin !== "string" && "origin is not a string") ||
@@ -149,10 +178,29 @@ function screenedGateOrThrow(v) {
149
178
  recordFailOpen("server.trace.gate-outcome-defective", "settlement.who is not an object with a party in the closed set");
150
179
  return undefined;
151
180
  }
181
+ const rel = d.kind === "allowed" ? d.released : undefined;
182
+ const released = rel === undefined
183
+ ? undefined
184
+ : {
185
+ mode: rel.mode,
186
+ askClass: rel.askClass,
187
+ classes: [...rel.classes],
188
+ ...(rel.origin === undefined ? {} : { origin: rel.origin }),
189
+ ...(rel.mandate === undefined ? {} : { mandate: rel.mandate }),
190
+ ...(rel.reason === undefined ? {} : { reason: rel.reason }),
191
+ };
152
192
  return {
153
193
  disposition: d.kind === "denied"
154
- ? { kind: "denied", deniedBy: d.deniedBy, ...(d.cause === undefined ? {} : { cause: d.cause }), ...(classifier === undefined ? {} : { classifier }) }
155
- : { kind: "allowed", ...(classifier === undefined ? {} : { classifier }) },
194
+ ? {
195
+ kind: "denied",
196
+ deniedBy: d.deniedBy,
197
+ ...(d.cause === undefined ? {} : { cause: d.cause }),
198
+ ...(classifier === undefined ? {} : { classifier }),
199
+ ...(d.mode === undefined ? {} : { mode: d.mode }),
200
+ ...(d.askClass === undefined ? {} : { askClass: d.askClass }),
201
+ ...(d.classes === undefined ? {} : { classes: [...d.classes] }),
202
+ }
203
+ : { kind: "allowed", ...(classifier === undefined ? {} : { classifier }), ...(released === undefined ? {} : { released }) },
156
204
  ...(settlement === undefined ? {} : { settlement }),
157
205
  ...(g.origin === undefined ? {} : { origin: g.origin }),
158
206
  };
@@ -168,7 +168,7 @@ export declare const WIRE_PROJECTION_FACES: {
168
168
  readonly wiring_manifest_tenant: {
169
169
  readonly projector: "wiringManifestEventData";
170
170
  readonly coreType: `WiringManifest & ${string}`;
171
- readonly excluded: readonly ["governance", "configFingerprint", "autoConsolidation", "readDeny", "peerPresence"];
171
+ readonly excluded: readonly ["governance", "configFingerprint", "autoConsolidation", "readDeny", "peerPresence", "permissionMode"];
172
172
  readonly unwrapped: readonly ["manifest"];
173
173
  readonly minted: readonly [];
174
174
  };
@@ -40,7 +40,7 @@ export const WIRE_PROJECTION_FACES = {
40
40
  wiring_manifest_tenant: {
41
41
  projector: "wiringManifestEventData",
42
42
  coreType: `WiringManifest & ${arm("wiring_manifest")}`,
43
- excluded: ["governance", "configFingerprint", "autoConsolidation", "readDeny", "peerPresence"],
43
+ excluded: ["governance", "configFingerprint", "autoConsolidation", "readDeny", "peerPresence", "permissionMode"],
44
44
  unwrapped: ["manifest"],
45
45
  minted: [],
46
46
  },
@@ -0,0 +1,110 @@
1
+ /**
2
+ * S-877 候裁 15 → B(主会话裁定 2026-09-29,收车档 ⑩)—— `UNATTENDED_PERMISSION_MODE`:**无人值守腿的模式,运维一处表态**。
3
+ *
4
+ * ## 规则(一句)
5
+ * 本仓**自己铸** spec、**没有调用方**能在请求上表态模式、也**没有审批席**的腿 —— leader worker(`src/leader/wire.ts`
6
+ * provisionWorker 两车道)、leader 的两只修复腿(集成沙箱修复 `mkRepair`、模块门修复 `runModuleGate` / 单代理修复环
7
+ * `attachRepairLoopDeps`)、run-local(`src/run-local.ts`,一次性 CLI)—— 它们的 `TaskSpec.permissionMode` 由本旋钮给,
8
+ * 经 {@link unattendedPermissionMode} 这**一只**读函数取值,各腿不各读 config、不各写默认。
9
+ *
10
+ * ## 为什么要有它(对 v7.105.0 回归的修,codex 对抗复审 F1 [high] 亲核成立)
11
+ * core 7.34.0 [ref]「Absent = `default`」:不带 `permissionMode` 的 spec 按 `default` 跑 —— 模式写门对每一次写先问、
12
+ * 形状层对非只读 shell 先问。上面三类腿**没有人**能答那一问(leader 的 Runner 不接 `onAsk`,零名单部署也不给 durable 席;
13
+ * run-local 无 TTY 时同形)⇒ headless 当场拒:worker `completed`、零写入。7.105.0 上这些腿不带模式也不问
14
+ * (缺席臂只给 base、`shellGate` 不写)。core 同段逐字给出路:「A library embedder that writes files with no approver
15
+ * now declares `permissionMode: "acceptEdits"` … or `"bypassPermissions"` … or wires an approver」——本仓对自己铸的
16
+ * 那几条 spec 就是那个 embedder。
17
+ *
18
+ * ## 三问
19
+ * · **谁需要**:用 leader / run-local 的部署(零审批名单是常态)—— 缺省值必须等于 7.105.0 的行为,否则升级即断。
20
+ * · **谁受伤**:想让无人值守腿更严的运维 —— **补偿** = 把旋钮设成 `acceptEdits`(根内写不问、非只读命令仍问 ⇒ 无人可答即拒)
21
+ * / `default` / `dontAsk`(一切会问的都拒)/ `plan`。部署的审批名单(`APPROVAL_REQUIRE` 等)与治理层的问(运维强制、ask 规则、
22
+ * hook)在任何一个词下都**照问**:bypassPermissions 只放行「普通」一类(core 7.34.0 [ref] 仲裁表,本仓不重推)。
23
+ * · **什么补偿**:缺省 = `bypassPermissions` —— 写门 / 形状层与 7.105.0 同(不问);词表外的值起服拒启([ref],不静默回缺省)。
24
+ * 候裁 18 → D:缺省 bypass 会释放写保护 ⇒ 本腿把写保护从问改成拒({@link createUnattendedWriteProtectionPolicy});另六类释放 = 相对 7.105.0 的宽化,
25
+ * 如实披露(见那只函数的「射程外」)。**过渡形**:core 7.37.0(S-906)后退成 `default` + 按类作答的 onAsk,旋钮硬删(拒启点名)。
26
+ *
27
+ * ## 写面:本旋钮只给模式的**仲裁**,不给 bypassPermissions 在 core 模式表里的**写面 open**
28
+ * core 7.34.0 起 `modePolicyOf("bypassPermissions").writeFace === "open"`(工作区根之外也能写)。7.105.0 上这几条腿**没有模式**,
29
+ * 写面恒是引擎缺省 `"roots"`。所以 {@link buildUnattendedLegSpec} 同时在任务席写 `writeFace: "roots"`(core 容纳面解析序:
30
+ * 显式任务席在模式那一行**之前**,`dist/tools/fs/containment-face.d.ts` resolveContainmentFace 第 3 / 3′ 行)—— 缺省与 7.105.0
31
+ * **逐字**同形,运维把旋钮设成任何词也不会顺手拿到根外写面(那是 HTTP 腿上 `TASK_WRITE_FACE_ESCALATION` 管的另一件事,
32
+ * S-591;这几条腿不是请求,不经那道门)。⚠️ 这一格是按裁定的**意图**(「= 7.105.0 行为」)施工、比裁定字面多出的一半,
33
+ * 收车档 ⑩ 单列,主会话可翻。
34
+ *
35
+ * ## 射程外(成文)
36
+ * · HTTP 腿(`POST /v1/tasks|runs`):模式在请求上(`permissionMode` / `settings.permissions.defaultMode`),缺席 = core `default`
37
+ * 原样过境(候裁 5 裁 A)—— 那里有调用方能表态,本旋钮不参与。
38
+ * · leader 的 planner 路由(无手 Runner,没有写 / shell 可问)不读本旋钮。
39
+ * · 中心下发 `RuntimeCaps.bypassPermissions === false` 时 core 把 bypass 折回 `default` 并发 `config.permission_mode_unavailable`
40
+ * —— 组织级拒优先。本仓今天不写这一位([ref] 未闭环),故这条今天只在 core 侧可达。
41
+ */
42
+ import { type PermissionMode, type RunnerDeps, type TaskSpec, type ToolPolicy } from "@sema-agent/core";
43
+ /** 旋钮的出厂值(主会话裁定候裁 15 B / 18 D)。写面由 {@link buildUnattendedLegSpec} 钉 roots;写保护由 {@link createUnattendedWriteProtectionPolicy} 改拒。 */
44
+ export declare const UNATTENDED_PERMISSION_MODE_DEFAULT: "bypassPermissions";
45
+ /** 解析结果:值 + 它从哪来(boot 一行 `unattended_permission_mode` 的两个字段)。 */
46
+ export interface UnattendedPermissionModeSetting {
47
+ readonly value: PermissionMode;
48
+ readonly source: "env" | "default";
49
+ }
50
+ /**
51
+ * `UNATTENDED_PERMISSION_MODE` 的**唯一**解析口。词表 = core `PERMISSION_MODES` 本尊(不手抄 —— core 加词本口随之受理,
52
+ * 减词本口随之拒启);两侧空白裁掉后**逐字**比较(不折大小写:`Default` / `BYPASS` 落拒启臂)。
53
+ * 未设 / 空串 ⇒ 出厂值,来源 `default`;词表外 ⇒ **拒启**,拒因点名旋钮与全部合法词([ref]:安全轴旋钮不许「以为设了其实没设」)。
54
+ */
55
+ export declare function parseUnattendedPermissionMode(raw: string | undefined): UnattendedPermissionModeSetting;
56
+ /** 三类无人值守腿的**唯一**读函数(见模块头注「规则」)。 */
57
+ export declare function unattendedPermissionMode(config: {
58
+ readonly unattendedPermissionMode: PermissionMode;
59
+ }): PermissionMode;
60
+ /** 无人值守腿铸进 spec 的两键(纯数据 ⇒ `build*`):模式 = 旋钮值;写面 = `"roots"`(理由见模块头注「写面」)。 */
61
+ export declare function buildUnattendedLegSpec(mode: PermissionMode): Required<Pick<TaskSpec, "permissionMode" | "writeFace">>;
62
+ /**
63
+ * 🔴 **S-877 候裁 18 → D(主会话续裁 2026-09-29)—— 无审批席的腿上,写保护从「问」改成「拒」**。
64
+ *
65
+ * **病**(codex 续车轮 R2-F1):core `MODE_ARBITRATION` 里 `bypassPermissions` 释放 `write_protection` 一类(`bypassImmune:false`)⇒ 旋钮缺省下写保护表里的
66
+ * 目标零询问放行;7.105.0 这些腿无模式 ⇒ 写保护是一问 ⇒ 无人可答 ⇒ 拒。core 7.35 没有「放普通问、留保护问」的模式,也没有限定模式可释放类的座。
67
+ *
68
+ * **规则(一条)**:本腿的模式**会释放**写保护一问时(读 core 仲裁本尊 `arbitrateByMode`,不手抄模式词),把**本腿引擎判官用的同一张写保护表**
69
+ * (`runnerSeats` = 构造本腿 Runner 的那份 deps 的两只座 {@link UnattendedLegRunnerSeats};`writeProtectedPaths` 缺席 = core 源表 `WRITE_PROTECTED_DEFAULT_TABLE`)经 core 的
70
+ * `compileWriteProtection` 编成判官,每一行铸成一条 **core 路径拒行**(`ToolPolicy.pathDenyRules`,`faces:["write"]`)—— 引擎的写面把它们并进本腿的拒表:
71
+ * 写工具的路径解析与 shell 站(重定向 / `tee` / `sed -i` / `cp`·`mv`·`touch` 操作数)都按拒表拒,模式**不释放拒**。**路径匹配全在 core**:每行的
72
+ * `reaches` 就是判官自己的 `matchPath`(同一只折叠、同一只数据根放行 —— `dataRoot` 与引擎判官同源:本腿 Runner 的 `memoryEngineDir`),本仓不写一个字的
73
+ * 路径比较。模式不释放写保护(`default` / `acceptEdits` / `plan` / `auto` / `dontAsk`)⇒ 不铸(那一问照旧问,有席的腿上人能答 —— run-local 有 TTY 时)。
74
+ *
75
+ * **射程外**(如实,收车档 ⑩ 候裁 18):缺省 bypass 还释放 `probe` / `tool_marks_mcp` / `tool_marks_a2a` / `transcript_write` / `rule_unreadable` /
76
+ * `artifact_shared_read` 六类(core 对它们没有「拒」的座),相对 7.105.0 是宽化 —— CHANGELOG「谁受伤」与 MIGRATION 写明,补偿 = 旋钮 `acceptEdits` / `default`。
77
+ * **过渡形**:core 7.37.0(S-906,`AskRequest.askClass?` / `classes?` 到货)后本仓改成 `default` 模式 + fail-closed 按类作答的 `onAsk`,本座与旋钮一并硬删。
78
+ * **续跑**(codex r3 F1):本层是活闭包、进不了 checkpoint,而 core 续跑在 spec 不说模式时取 checkpoint 记下的模式 ⇒ durable 续跑必须重挂整条腿 ——
79
+ * leader worker 的续跑上下文带 `CheckpointCtx.unattendedLeg`,续跑家族据此让 `resolveSpec` 在全部折叠之后调 {@link createUnattendedLeg} / {@link withUnattendedLeg}。
80
+ * **已知过拒**(codex r3 F2,fail-closed 方向,随过渡形退役):core 的路径拒座在 shell 站把 `cp` 的**源**操作数也按写目标判(CC 同形,cli280.js:216993
81
+ * `cp:"write"`),引擎写保护判官只判真写目标 ⇒ 从写保护目标复制出来在本层下被拒、7.105.0 上照跑。本仓不补路径比较(那是第二个判官),特征化钉在
82
+ * test/run-local.test.ts「🚧 codex r3 F2」。
83
+ * **附带效果**(S-877 四续 ⑫b,随过渡形退役):本层只能是一只 `ToolPolicy`(路径拒座挂在策略上,`check` 必填 —— 这里恒 `allow`),原先没有 toolPolicy
84
+ * 的腿因此被引擎读成「有策略层」:系统提示多 `# Execution environment` 一块、UNGATED 告警不再发。7.35.2 上表达不了「只供拒行、不算策略层」;
85
+ * 特征化钉在 test/unattended-permission-mode.test.ts「🚧 四续 ⑫b」。
86
+ *
87
+ * 返回 `undefined` = 本腿不需要这一层(模式不释放写保护,或运维把表清空 `[]`)。活对象(带判官闭包)⇒ `create*`。
88
+ */
89
+ export declare function createUnattendedWriteProtectionPolicy(mode: PermissionMode, runnerSeats: UnattendedLegRunnerSeats): ToolPolicy | undefined;
90
+ /** 无人值守腿的全部铸件:模式 + 写面(纯数据)与写保护拒层(活对象,可缺席)。由 {@link createUnattendedLeg} 铸、{@link withUnattendedLeg} 并进 spec。 */
91
+ export interface UnattendedLeg {
92
+ readonly spec: Required<Pick<TaskSpec, "permissionMode" | "writeFace">>;
93
+ readonly writeProtection: ToolPolicy | undefined;
94
+ }
95
+ /**
96
+ * 引擎写保护判官读的两只 Runner 座(`RunnerDeps` 的同名切片):表(`writeProtectedPaths`,缺席 = core 源表)与数据根放行(`memoryEngineDir`,缺席 = 引擎缺省)。
97
+ * 调用方递**构造本腿 Runner 的那一份** deps(或它的展开源)—— 表与根从哪来、根要不要在,都由那份 deps 说了算,腿的接线处不再单独读任何一只坐标
98
+ * (本模块是唯一读者;S-877 四续:接线处各读一份就是「两边同一张表、同一个根」这句话的第二、三个写者)。
99
+ */
100
+ export type UnattendedLegRunnerSeats = Pick<RunnerDeps, "writeProtectedPaths" | "memoryEngineDir">;
101
+ /** 三处无人值守腿(leader worker / 修复腿 / run-local)与续跑重挂的**唯一**铸点:`mode` 取 {@link unattendedPermissionMode},`runnerSeats` = 本腿 Runner 的 deps
102
+ * (引擎写保护判官与本层拒行同一张表、同一个数据根)。 */
103
+ export declare function createUnattendedLeg(mode: PermissionMode, runnerSeats: UnattendedLegRunnerSeats): UnattendedLeg;
104
+ /**
105
+ * 把无人值守腿并进一条 spec —— 三处腿的**唯一**合并口:两键覆盖(本腿的模式与写面由旋钮说了算),写保护拒层与 spec 已有的 `toolPolicy` **合成**
106
+ * (`combinePolicies`,deny-wins,携带 `pathDenyRules`)而不是覆盖(leader worker 的 durable 名单门、run-local 的治理链都在那里)。
107
+ * 调用点必须在 spec 其余键(含治理折叠)**之后**调它,否则后铸的 `toolPolicy` 会把拒层盖掉。
108
+ */
109
+ export declare function withUnattendedLeg<S extends Partial<TaskSpec>>(spec: S, leg: UnattendedLeg): S & Required<Pick<TaskSpec, "permissionMode" | "writeFace">>;
110
+ //# sourceMappingURL=unattended-permission-mode.d.ts.map
@@ -0,0 +1,65 @@
1
+ import { ASK_CLASSES, PERMISSION_MODES, arbitrateByMode, combinePolicies, compileWriteProtection, foldArbitrationRow } from "@sema-agent/core";
2
+ import { recordFailOpen } from "./observability/fail-open.js";
3
+ export const UNATTENDED_PERMISSION_MODE_DEFAULT = "bypassPermissions";
4
+ export function parseUnattendedPermissionMode(raw) {
5
+ const trimmed = (raw ?? "").trim();
6
+ if (trimmed === "")
7
+ return { value: UNATTENDED_PERMISSION_MODE_DEFAULT, source: "default" };
8
+ const word = PERMISSION_MODES.find((m) => m === trimmed);
9
+ if (word === undefined) {
10
+ throw new Error(`env UNATTENDED_PERMISSION_MODE="${trimmed.slice(0, 120)}" is not a permission mode — use one of: ${PERMISSION_MODES.join(", ")} ` +
11
+ `(or leave it unset for the default "${UNATTENDED_PERMISSION_MODE_DEFAULT}", which keeps the unattended legs — leader workers / repairs and run-local — acting as they did before server 7.106.0). ` +
12
+ `The word is matched EXACTLY (surrounding whitespace trimmed, no case folding). ` +
13
+ `Refusing to boot rather than treating it as unset: that would run those legs under a mode the operator did not choose`);
14
+ }
15
+ return { value: word, source: "env" };
16
+ }
17
+ export function unattendedPermissionMode(config) {
18
+ return config.unattendedPermissionMode;
19
+ }
20
+ export function buildUnattendedLegSpec(mode) {
21
+ return { permissionMode: mode, writeFace: "roots" };
22
+ }
23
+ export function createUnattendedWriteProtectionPolicy(mode, runnerSeats) {
24
+ if (!modeReleases(mode, "write_protection"))
25
+ return undefined;
26
+ const dataRoot = runnerSeats.memoryEngineDir;
27
+ const judge = compileWriteProtection(runnerSeats.writeProtectedPaths, dataRoot !== undefined ? { dataRoot } : undefined);
28
+ if (judge === undefined)
29
+ return undefined;
30
+ const pathDenyRules = judge.rows.map((row) => ({
31
+ rule: `WriteProtected(${row.name})`,
32
+ tool: "Edit",
33
+ pattern: row.name,
34
+ faces: ["write"],
35
+ reaches: (absolutePath) => judge.matchPath(absolutePath)?.name === row.name,
36
+ }));
37
+ return {
38
+ check: () => ({ action: "allow" }),
39
+ pathDenyRules,
40
+ };
41
+ }
42
+ function modeReleases(mode, askClass) {
43
+ const classes = new Set([askClass]);
44
+ return arbitrateByMode(mode, { classes, askClass, row: foldArbitrationRow(classes) }).kind === "released";
45
+ }
46
+ const INTENDED_RELEASES = new Set(["ordinary", "mode"]);
47
+ function releasedProtectiveClasses(mode) {
48
+ return ASK_CLASSES.filter((c) => !INTENDED_RELEASES.has(c) && c !== "write_protection" && modeReleases(mode, c));
49
+ }
50
+ const RELEASED_PROTECTIVE_ASKS_TAG = "server.unattended-leg.mode-releases-protective-asks";
51
+ export function createUnattendedLeg(mode, runnerSeats) {
52
+ const debt = releasedProtectiveClasses(mode);
53
+ if (debt.length > 0)
54
+ recordFailOpen(RELEASED_PROTECTIVE_ASKS_TAG, `mode=${mode} released=${debt.join(",")}`);
55
+ return { spec: buildUnattendedLegSpec(mode), writeProtection: createUnattendedWriteProtectionPolicy(mode, runnerSeats) };
56
+ }
57
+ export function withUnattendedLeg(spec, leg) {
58
+ const wp = leg.writeProtection;
59
+ return {
60
+ ...spec,
61
+ ...leg.spec,
62
+ ...(wp !== undefined ? { toolPolicy: spec.toolPolicy !== undefined ? combinePolicies(spec.toolPolicy, wp) : wp } : {}),
63
+ };
64
+ }
65
+ //# sourceMappingURL=unattended-permission-mode.js.map