@sema-agent/server 7.4.0 → 7.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +18 -3
  3. package/README.zh-CN.md +14 -3
  4. package/USAGE.md +80 -1
  5. package/dist/approval-card.d.ts +15 -3
  6. package/dist/approval-card.js +41 -7
  7. package/dist/approval-reconciler.d.ts +109 -12
  8. package/dist/approval-reconciler.js +152 -24
  9. package/dist/boot/config-center.js +15 -2
  10. package/dist/boot/coordinators.js +10 -2
  11. package/dist/boot/execution-env.js +1 -1
  12. package/dist/boot/org-memory.d.ts +6 -0
  13. package/dist/boot/org-memory.js +1 -1
  14. package/dist/boot/parked-revive-gate.d.ts +78 -0
  15. package/dist/boot/parked-revive-gate.js +114 -0
  16. package/dist/boot/reapers.d.ts +2 -0
  17. package/dist/boot/reapers.js +11 -4
  18. package/dist/boot/resolve-spec.d.ts +3 -19
  19. package/dist/boot/resolve-spec.js +73 -67
  20. package/dist/boot/runner-deps.d.ts +23 -1
  21. package/dist/boot/runner-deps.js +8 -11
  22. package/dist/boot/workflow-orchestration.d.ts +8 -3
  23. package/dist/boot/workflow-orchestration.js +23 -1
  24. package/dist/budget.d.ts +1 -1
  25. package/dist/budget.js +1 -1
  26. package/dist/capabilities/repo-tools.d.ts +1 -1
  27. package/dist/capabilities/repo-tools.js +8 -2
  28. package/dist/config-center/apply-effective.js +33 -10
  29. package/dist/config-provider.d.ts +1 -0
  30. package/dist/config-provider.js +23 -3
  31. package/dist/config-types.d.ts +27 -9
  32. package/dist/config.d.ts +6 -1
  33. package/dist/config.js +61 -15
  34. package/dist/deployment-governance.d.ts +168 -0
  35. package/dist/deployment-governance.js +206 -0
  36. package/dist/env-facts.d.ts +3 -1
  37. package/dist/env-facts.js +3 -1
  38. package/dist/fleet/fleet-bus.d.ts +17 -2
  39. package/dist/fleet/fleet-bus.js +68 -3
  40. package/dist/fleet/fleet-terminal-window.d.ts +98 -0
  41. package/dist/fleet/fleet-terminal-window.js +316 -0
  42. package/dist/governance-ask-marks.d.ts +31 -0
  43. package/dist/governance-ask-marks.js +122 -0
  44. package/dist/hooks/hook-runner.d.ts +28 -0
  45. package/dist/hooks/hook-runner.js +149 -25
  46. package/dist/http/routes/approvals-assistant.js +6 -7
  47. package/dist/http/routes/diagnostics.js +10 -5
  48. package/dist/http/routes/fleet.js +160 -14
  49. package/dist/http/routes/memory-policy.d.ts +2 -1
  50. package/dist/http/routes/memory-policy.js +77 -13
  51. package/dist/http/routes/runs.js +6 -2
  52. package/dist/http/routes/tasks.js +59 -22
  53. package/dist/http/routes/trace-usage.js +3 -4
  54. package/dist/http/send.d.ts +23 -0
  55. package/dist/http/send.js +23 -0
  56. package/dist/http/server.d.ts +9 -0
  57. package/dist/http/server.js +28 -14
  58. package/dist/http/sse-log.js +3 -4
  59. package/dist/http/wire-types.d.ts +7 -2
  60. package/dist/leader/diffout.d.ts +10 -0
  61. package/dist/leader/diffout.js +14 -2
  62. package/dist/leader/diffup.js +3 -2
  63. package/dist/leader/planner.js +7 -0
  64. package/dist/main.js +39 -31
  65. package/dist/observability/fail-open.d.ts +17 -2
  66. package/dist/observability/fail-open.js +19 -4
  67. package/dist/observability/prompt-manifest.d.ts +5 -1
  68. package/dist/orchestration/workflow-notify-journal.d.ts +58 -2
  69. package/dist/orchestration/workflow-notify-journal.js +130 -45
  70. package/dist/parked-decide.js +9 -4
  71. package/dist/plugins/approval-ask-store-memory.d.ts +2 -2
  72. package/dist/plugins/approval-ask-store-memory.js +3 -2
  73. package/dist/plugins/approval-ask-store-sql.d.ts +60 -5
  74. package/dist/plugins/approval-ask-store-sql.js +75 -35
  75. package/dist/plugins/background-agent-store-sql.js +16 -16
  76. package/dist/plugins/background-shell-support.d.ts +1 -1
  77. package/dist/plugins/background-shell-support.js +2 -2
  78. package/dist/plugins/breaker-state-sql.js +2 -2
  79. package/dist/plugins/checkpoint-store-sql.d.ts +67 -8
  80. package/dist/plugins/checkpoint-store-sql.js +76 -13
  81. package/dist/plugins/image-bake-store-sql.d.ts +1 -1
  82. package/dist/plugins/image-bake-store-sql.js +27 -27
  83. package/dist/plugins/image-index-sql.js +15 -15
  84. package/dist/plugins/local-checkpoint-store.d.ts +20 -1
  85. package/dist/plugins/local-checkpoint-store.js +19 -0
  86. package/dist/plugins/mailbox-store-sql.d.ts +4 -10
  87. package/dist/plugins/mailbox-store-sql.js +59 -6
  88. package/dist/plugins/memory-engine-pg.js +9 -9
  89. package/dist/plugins/memory-engine-tidb.js +7 -7
  90. package/dist/plugins/memory-sync-store-pg.js +13 -13
  91. package/dist/plugins/memory-sync-store-tidb.js +5 -5
  92. package/dist/plugins/outcome-ledger-sql.js +7 -7
  93. package/dist/plugins/pg-cost-quota.js +3 -3
  94. package/dist/plugins/pg-pool.js +84 -75
  95. package/dist/plugins/pg-rate-limiter.js +3 -3
  96. package/dist/plugins/pg-session-storage.d.ts +1 -1
  97. package/dist/plugins/pg-session-storage.js +12 -13
  98. package/dist/plugins/remote-env-host.js +3 -1
  99. package/dist/plugins/remote-env-local-docker.js +6 -3
  100. package/dist/plugins/remote-env-ssh.d.ts +13 -1
  101. package/dist/plugins/roster-store-sql.js +8 -8
  102. package/dist/plugins/store-contracts.d.ts +19 -0
  103. package/dist/plugins/store-contracts.js +42 -0
  104. package/dist/plugins/task-attachment-store.js +5 -5
  105. package/dist/plugins/task-list-store-sql.js +1 -1
  106. package/dist/plugins/tidb-cost-quota.js +1 -1
  107. package/dist/plugins/tidb-pool.js +83 -60
  108. package/dist/plugins/tidb-rate-limiter.js +1 -1
  109. package/dist/plugins/tidb-session-store.js +2 -5
  110. package/dist/plugins/tool-result-store-sql.js +2 -2
  111. package/dist/plugins/usage-window-store-sql.js +13 -13
  112. package/dist/plugins/write-behind-counter.d.ts +10 -2
  113. package/dist/plugins/write-behind-counter.js +13 -3
  114. package/dist/resource-suspend.d.ts +3 -1
  115. package/dist/resource-suspend.js +3 -1
  116. package/dist/run-local.d.ts +73 -1
  117. package/dist/run-local.js +146 -5
  118. package/dist/runs.d.ts +11 -1
  119. package/dist/runs.js +18 -3
  120. package/dist/runtime-governance.d.ts +18 -0
  121. package/dist/runtime-governance.js +90 -3
  122. package/dist/security.d.ts +12 -0
  123. package/dist/security.js +12 -0
  124. package/dist/session-sync-kernel.d.ts +13 -0
  125. package/dist/session-sync-kernel.js +13 -0
  126. package/dist/task-settings.d.ts +3 -9
  127. package/dist/task-settings.js +16 -13
  128. package/dist/tool-approval.d.ts +33 -6
  129. package/dist/tool-approval.js +80 -23
  130. package/dist/trace/core-keyset-guard.d.ts +18 -4
  131. package/dist/trace/project.d.ts +10 -1
  132. package/dist/trace/project.js +31 -0
  133. package/package.json +3 -3
  134. package/dist/boot/lexical-path-env.d.ts +0 -10
  135. package/dist/boot/lexical-path-env.js +0 -88
  136. package/dist/capabilities/oa-tools.d.ts +0 -15
  137. package/dist/capabilities/oa-tools.js +0 -54
  138. package/dist/finance/cost-taxonomy.d.ts +0 -34
  139. package/dist/finance/cost-taxonomy.js +0 -26
  140. package/dist/plugins/approval-store-sql.d.ts +0 -116
  141. package/dist/plugins/approval-store-sql.js +0 -151
  142. package/dist/plugins/file-workflow-journal-store.d.ts +0 -12
  143. package/dist/plugins/file-workflow-journal-store.js +0 -12
  144. package/dist/plugins/pg-approval-store.d.ts +0 -9
  145. package/dist/plugins/pg-approval-store.js +0 -9
  146. package/dist/plugins/pg-breaker-state.d.ts +0 -8
  147. package/dist/plugins/pg-breaker-state.js +0 -8
  148. package/dist/plugins/pg-checkpoint-store.d.ts +0 -10
  149. package/dist/plugins/pg-checkpoint-store.js +0 -10
  150. package/dist/plugins/pg-file-snapshot-store.d.ts +0 -8
  151. package/dist/plugins/pg-file-snapshot-store.js +0 -8
  152. package/dist/plugins/pg-image-bake.d.ts +0 -12
  153. package/dist/plugins/pg-image-bake.js +0 -11
  154. package/dist/plugins/pg-image-index.d.ts +0 -12
  155. package/dist/plugins/pg-image-index.js +0 -11
  156. package/dist/plugins/pg-outcome-ledger.d.ts +0 -12
  157. package/dist/plugins/pg-outcome-ledger.js +0 -11
  158. package/dist/plugins/pg-resume-anchor-store.d.ts +0 -7
  159. package/dist/plugins/pg-resume-anchor-store.js +0 -7
  160. package/dist/plugins/pg-run-store.d.ts +0 -9
  161. package/dist/plugins/pg-run-store.js +0 -9
  162. package/dist/plugins/pg-session-policy-store.d.ts +0 -7
  163. package/dist/plugins/pg-session-policy-store.js +0 -7
  164. package/dist/plugins/pg-session-store.d.ts +0 -12
  165. package/dist/plugins/pg-session-store.js +0 -12
  166. package/dist/plugins/pg-tool-result-store.d.ts +0 -9
  167. package/dist/plugins/pg-tool-result-store.js +0 -9
  168. package/dist/plugins/pg-workflow-journal-store.d.ts +0 -9
  169. package/dist/plugins/pg-workflow-journal-store.js +0 -9
  170. package/dist/plugins/pg-workflow-run-store.d.ts +0 -9
  171. package/dist/plugins/pg-workflow-run-store.js +0 -9
  172. package/dist/plugins/tidb-approval-store.d.ts +0 -8
  173. package/dist/plugins/tidb-approval-store.js +0 -8
  174. package/dist/plugins/tidb-breaker-state.d.ts +0 -7
  175. package/dist/plugins/tidb-breaker-state.js +0 -7
  176. package/dist/plugins/tidb-checkpoint-store.d.ts +0 -9
  177. package/dist/plugins/tidb-checkpoint-store.js +0 -9
  178. package/dist/plugins/tidb-file-snapshot-store.d.ts +0 -8
  179. package/dist/plugins/tidb-file-snapshot-store.js +0 -8
  180. package/dist/plugins/tidb-image-bake.d.ts +0 -12
  181. package/dist/plugins/tidb-image-bake.js +0 -11
  182. package/dist/plugins/tidb-image-index.d.ts +0 -12
  183. package/dist/plugins/tidb-image-index.js +0 -11
  184. package/dist/plugins/tidb-outcome-ledger.d.ts +0 -12
  185. package/dist/plugins/tidb-outcome-ledger.js +0 -12
  186. package/dist/plugins/tidb-resume-anchor-store.d.ts +0 -7
  187. package/dist/plugins/tidb-resume-anchor-store.js +0 -7
  188. package/dist/plugins/tidb-run-store.d.ts +0 -10
  189. package/dist/plugins/tidb-run-store.js +0 -9
  190. package/dist/plugins/tidb-session-policy-store.d.ts +0 -7
  191. package/dist/plugins/tidb-session-policy-store.js +0 -7
  192. package/dist/plugins/tidb-tool-result-store.d.ts +0 -8
  193. package/dist/plugins/tidb-tool-result-store.js +0 -10
  194. package/dist/plugins/tidb-workflow-journal-store.d.ts +0 -9
  195. package/dist/plugins/tidb-workflow-journal-store.js +0 -9
  196. package/dist/plugins/tidb-workflow-run-store.d.ts +0 -10
  197. package/dist/plugins/tidb-workflow-run-store.js +0 -10
  198. package/dist/plugins/workflow-journal-limits.d.ts +0 -12
  199. package/dist/plugins/workflow-journal-limits.js +0 -12
  200. package/dist/sema-registry.d.ts +0 -41
  201. package/dist/sema-registry.js +0 -40
@@ -192,16 +192,10 @@ export interface FsWriteGateWiring {
192
192
  * allow with decisionReason "rule", a THROWN probe ⇒ not exempt = fail-closed to ask). Same canonical
193
193
  * toolName key space as the ask-policy layer's exemption probe — ONE grant serves both layers; the gate-level
194
194
  * check is what makes "allow all session" bite SAME-TURN for the parent and inherited child tasks (the ask
195
- * path is never re-entered). (codex M1 的边界注记已闭合:core 1.295 起本仓在 gate 腿上组合
196
- * createSensitivePathPolicy —— `sensitivePatterns`,deny combinePolicies 折叠里恒胜,豁免/acceptDirs
197
- * 越不过它。) */
195
+ * path is never re-entered). (codex M1 的边界注记:sensitive-path DENY 腿与本探针的「豁免越不过 deny」
196
+ * 保证仍成立——但那条腿已随 #177 搬到 governance 层,deny-wins tightenTaskSpec 折叠给的,不再依赖
197
+ * 本 wiring 的组合点。) */
198
198
  isExempt?: (toolName: string, canonicalPath: string) => boolean | Promise<boolean>;
199
- /** ③ (core 1.295): the sensitive-path write DENY set composed onto the fs-write gate leg —
200
- * `createSensitivePathPolicy({env, patterns, rootPath: cwd})`, deny-wins in the SAME combinePolicies fold, so a
201
- * session exemption (isExempt) or an acceptEdits auto-allow can never cross it (the 1.199-batch documented gap,
202
- * now closed). Source = config `SENSITIVE_WRITE_PATTERNS` (unset ⇒ core RECOMMENDED_SENSITIVE_PATTERNS; the
203
- * set curation is core's — the server passes through). Absent/empty ⇒ no sensitive policy (pre-1.295 shape). */
204
- sensitivePatterns?: string[];
205
199
  }
206
200
  export declare function deriveSettingsPolicy(settings: ParsedTaskSettings, gate?: FsWriteGateWiring, workflowGate?: WorkflowGateWiring): {
207
201
  toolPolicy?: ToolPolicy;
@@ -35,8 +35,8 @@
35
35
  * - (`hooks` NO LONGER deferred either — 轴B #2 注释保鲜:applied 形已接线(本文件 hooks 分片 +
36
36
  * main.ts 单用户闸),仅 malformed 才报 deferred;旧「需要 remote hook-runner」是接线前拍照。)
37
37
  */
38
- import { combinePolicies, createAllowDenyPolicy, createFsWriteGatePolicy, createSensitivePathPolicy, tightenTaskSpec, resolveReasoningProfile, rankOf, isThinkingLevel, CYBER_RISK, URL_SAFETY, HARNESS_SECTION_ANCHOR, } from "@sema-agent/core";
39
- import { findUnmatchableToolNames, formatUnmatchableToolNames } from "./config.js";
38
+ import { combinePolicies, createAllowDenyPolicy, createFsWriteGatePolicy, tightenTaskSpec, resolveReasoningProfile, rankOf, isThinkingLevel, CYBER_RISK, URL_SAFETY, HARNESS_SECTION_ANCHOR, } from "@sema-agent/core";
39
+ import { findCcRuleFormNames, findUnmatchableToolNames, formatUnmatchableToolNames } from "./config.js";
40
40
  import { parseHooksConfig } from "./hooks/hook-runner.js";
41
41
  /**
42
42
  * R4: coerce a raw top-level `body.permissionMode` (the LIGHT per-turn wire — the shell sends the
@@ -367,9 +367,12 @@ function createWorkflowPlanDenyPolicy() {
367
367
  * silent on this leg. Throwing here surfaces as a 422 at the HTTP boundary (resolve-spec already wraps every
368
368
  * throw out of `applyTaskSettings`), i.e. the same fail-loud posture the other two legs have.
369
369
  *
370
- * `allow` is deliberately NOT judged: it is a WHITELIST — a name that matches nothing leaves the allowlist
371
- * effectively empty, so every tool falls to "not in the allowlist" and is DENIED. That fails closed and is
372
- * immediately visible to the caller, which is a different (and acceptable) failure mode from a silent hole.
370
+ * `allow` is deliberately NOT judged by the GENERAL arm: it is a WHITELIST — a name that matches nothing leaves
371
+ * the allowlist effectively empty, so every tool falls to "not in the allowlist" and is DENIED. That fails closed
372
+ * and is immediately visible to the caller, which is a different (and acceptable) failure mode from a silent hole.
373
+ * The ONE exception is the CC-rule-form arm ({@link findCcRuleFormNames}, #186): `allow: ["Bash(ps:*)"]` is the
374
+ * shape a CC user writes to get FEWER prompts and it silently costs them Bash entirely — a spelling-family error,
375
+ * not a typo, so it is refused on all three lists. See the config.ts docblock for the two knobs it points at.
373
376
  */
374
377
  function rejectUnmatchableSettingsNames(perms) {
375
378
  for (const [field, names] of [["permissions.ask", perms.ask], ["permissions.deny", perms.deny]]) {
@@ -379,6 +382,11 @@ function rejectUnmatchableSettingsNames(perms) {
379
382
  if (bad.length > 0)
380
383
  throw new Error(formatUnmatchableToolNames(field, bad));
381
384
  }
385
+ if (perms.allow) {
386
+ const bad = findCcRuleFormNames(perms.allow);
387
+ if (bad.length > 0)
388
+ throw new Error(formatUnmatchableToolNames("permissions.allow", bad));
389
+ }
382
390
  }
383
391
  export function deriveSettingsPolicy(settings, gate, workflowGate) {
384
392
  const perms = settings.permissions;
@@ -398,11 +406,8 @@ export function deriveSettingsPolicy(settings, gate, workflowGate) {
398
406
  // the given env, only ever allow/ask). undefined without wiring (see FsWriteGateWiring). #156/#165: the sandbox
399
407
  // lanes supply wiring too — same factory, a deferred real-env proxy and no cwd; the shape difference lives
400
408
  // entirely in what `resolve-spec.ts` puts in the wiring, not in a second code path here.
401
- // ③ (core 1.295): the sensitive-path DENY policy (patterns from the wiring config SENSITIVE_WRITE_PATTERNS,
402
- // default = core's RECOMMENDED set) composes into the SAME fold, deny-wins — core semantics deny/"safety", so
403
- // neither the gate's own allow legs (exemptDirs/acceptDirs/isExempt session exemption) nor a settings allow can
404
- // cross it (the 1.199-batch gap: an exempted tool could write .git/hooks — closed here). Same env/rootPath as
405
- // the gate (the fs the hands really write; symlink canonicalization inside the policy, dist-read).
409
+ // (③ sensitive-path DENY 已随 #177 governance 层——见 FsWriteGateWiring 尾注。deny-wins
410
+ // tightenTaskSpec 折叠保证:豁免/acceptDirs/settings allow 仍然越不过部署 DENY 基线。)
406
411
  const fsWriteGate = (acceptCwd) => {
407
412
  if (!gate)
408
413
  return undefined;
@@ -422,9 +427,7 @@ export function deriveSettingsPolicy(settings, gate, workflowGate) {
422
427
  // NotebookEdit 时 notebook_path 恒先(诱饵 file_path 失效),其余工具 file_path 先(反向诱饵也封);均 dist
423
428
  // 亲读。回归锚保留在 test/task-settings.test.ts(NotebookEdit deny + 双向诱饵),现在锁的是 core 行为经
424
429
  // 我方折叠——core 若回退,锚变红。
425
- return gate.sensitivePatterns && gate.sensitivePatterns.length > 0
426
- ? combinePolicies(createSensitivePathPolicy({ env: gate.env, patterns: gate.sensitivePatterns, ...(gate.cwd !== undefined ? { rootPath: gate.cwd } : {}) }), gatePolicy)
427
- : gatePolicy;
430
+ return gatePolicy;
428
431
  };
429
432
  const compose = (...extra) => {
430
433
  const all = [...policies, ...extra.filter((p) => p !== undefined)];
@@ -1,6 +1,7 @@
1
1
  import { type AskRequest, type AskOutcome } from "@sema-agent/core";
2
2
  import { type ApprovalRequestFrame, type ApprovalRevokeFrame } from "./approval-card.js";
3
3
  import type { ApprovalAskStore } from "./plugins/approval-ask-store-sql.js";
4
+ import { type GovernanceAskMarks } from "./governance-ask-marks.js";
4
5
  /** A live approval frame delivered to whoever tails this run's stream. `type` IS the SSE event name (named-event
5
6
  * convention, same as question). The shell renders `tool_approval` as the CC three-choice card and dismisses on
6
7
  * `tool_approval_complete`. */
@@ -37,6 +38,19 @@ export interface ToolApprovalFrame {
37
38
  agentName?: string;
38
39
  };
39
40
  message?: string;
41
+ /**
42
+ * [2942]/[2943] **ADDITIVE**,`"tool_approval"` only —— `true` ⇔ 这只 ask 的门来自**运维治理层**
43
+ * (`AUTONOMY` / `commandPolicy` / `MANUAL_MODE_SHELL_GATE` / `SENSITIVE_WRITE_PATTERNS` 合成的那条
44
+ * policy,`applyRuntimeGovernance`),而不是模型默认门或客户端表态。壳据此渲染「治理强制」徽标,回答
45
+ * [3031]#1/[3038] 实证的那个 UX 缺口:「我都开 bypassPermissions 了为什么还在问」。
46
+ *
47
+ * 🔴 **缺席 ≠ `false`**:本键**只在为真时在场**,缺席的含义是「没有治理来源的证据」——既覆盖真正的
48
+ * 非治理 ask(如 `APPROVAL_REQUIRE` 显式列名),也覆盖判据够不着的形(如治理层 shellGate 停在
49
+ * `"classify"` 档时,一次 shell ask 究竟出自分类器还是别的门,在 `AskRequest` 上无从分辨)。
50
+ * 消费端**禁**把缺席读成「这不是治理门」。判定缝的真形与为什么不能走 `decisionReason`:
51
+ * `governance-ask-marks.ts` 顶注。
52
+ */
53
+ governanceForced?: true;
40
54
  /** "tool_approval" only: the tool call's args, secret-redacted, UNTRUSTED-for-display. Absent (with
41
55
  * `argsOmitted: true`) when over the byte cap or unserializable. */
42
56
  args?: unknown;
@@ -61,8 +75,10 @@ export interface ToolApprovalRunContext {
61
75
  * 协议**(`approval_request` 族)的一员,消费方是新壳 —— 分口投递是诚实的分层,装配点(车3 3b /
62
76
  * 车4 域)可以与 `approval_request` 的发射点**同批**接上。
63
77
  *
64
- * 缺席 ⇒ 本连接不收撤卡帧(与 `approval_request` 今天尚未接上发射点同栏);壳侧的结构补偿恒是重连
65
- * preamble 的全量对账基准。
78
+ * 缺席 ⇒ 本连接不收撤卡帧;壳侧的结构补偿恒是重连 preamble 的全量对账基准。
79
+ * (原注拿「`approval_request` 今天也还没接上发射点」作同栏类比 —— 那个类比已被 60846e6 证伪:呈卡口
80
+ * 三腿都接上了,而本钩子至今只有 sync 腿接 live 口。两者不再同栏,类比句因此删掉,别再据它推断
81
+ * 「两族都还没接线」。)
66
82
  */
67
83
  emitRevoke?: (frame: ApprovalRevokeFrame) => void | Promise<void>;
68
84
  /**
@@ -115,7 +131,8 @@ export declare function parseToolApprovalResponse(body: unknown): {
115
131
  *
116
132
  * 五个合取项,缺一即不上场(每一项的缺席都有它自己的 `reason`,供启动期 info 与诊断分辨):
117
133
  * 1. `toolApprovalEnabled` —— 连活卡腿都没有,谈不上流内协议;
118
- * 2. `streamApprovalEnabled` —— 协议总开关(默认 OFF);
134
+ * 2. `streamApprovalEnabled` —— 协议总开关(`STREAM_APPROVAL_ENABLED`,树上已**默认 ON**;显式 `false`
135
+ * 是唯一干净还原键。默认极性与版本坐标的属主口径见 `config.ts` 的 `streamApprovalConfig()` 头注);
119
136
  * 3. `backend` 在场 —— env-only worker 没有 `StoreBackend` 本体;
120
137
  * 4. **账必须是持久的**(`kind !== "local"`)—— InMemory 形重启即丢**已接受的决议**,那与「对账便利
121
138
  * 丢失」不是一个量级(§2.2(b));File 形 ask 店落地后这一项自然翻真;
@@ -135,9 +152,13 @@ export type StreamApprovalGate = {
135
152
  * 协调器的家,不该为一个布尔判据把整棵 store 依赖树拖进类型面。 */
136
153
  export interface StreamApprovalGateInput {
137
154
  toolApprovalEnabled: boolean;
138
- /** 协议总开关。⚠️ 调用点一律写 `config.streamApproval?.enabled === true` —— **可选链是 fail-closed 的**:
139
- * 配置段缺席只可能意味着「这个部署没有配它」,而协议的默认就是 OFF。真实 boot 恒有此段
140
- * (`config.ts` 无条件铸 + 白名单门盯着),`?.` 服务的是那些只填被测路由用得到的键的 stub-harness。 */
155
+ /** 协议总开关。⚠️ 调用点一律写 `config.streamApproval?.enabled === true` —— **可选链的缺席臂折算
156
+ * false**(段整段缺席 协议不上场)。
157
+ * 🔴 当年这条写的理由是「缺席折算值 == 产品默认(那时是 OFF)」,所以 `?.` 纯粹无副作用;产品默认
158
+ * 现已翻 ON(`config.ts` 的 `streamApprovalConfig()`),两者**不再相等**,那条理由作废。现行的
159
+ * (更弱也更诚实的)判据:真实 boot 恒有此段(`config.ts` 无条件铸 + 白名单门盯着),`?.` 服务的只是
160
+ * 那些只填被测路由用得到的键的 stub-harness,而一个没填这个段的 harness 要的必然是「别上场」。
161
+ * **不要**照「缺席 = 与产品默认同」去推理。 */
141
162
  streamApprovalEnabled: boolean;
142
163
  backend: {
143
164
  readonly kind: "mysql" | "pg" | "local";
@@ -276,12 +297,18 @@ export declare class ToolApprovalCoordinator {
276
297
  /** #151 车2(D5 一次性 warn 节流):同实例只报第一次,后续只计数(避免 store 抖动期间刷屏)。 */
277
298
  private storeErrorWarned;
278
299
  private storeErrorTally;
300
+ /** [2942]/[2943] `governanceForced` 的**读侧**表(写侧 = runtime-governance 的两只观察器)。
301
+ * 缺席(生产形)⇒ 每条 run 腿由 {@link runWithContext} 现铸一张、经 ALS 与写侧共享;在场 ⇒ 测试注入的
302
+ * 固定表(此时不进 ALS 作用域,读写都走这一张)。作用域理由见 `governance-ask-marks.ts` 顶注。 */
303
+ private readonly governanceAskMarks;
279
304
  constructor(opts?: {
280
305
  ttlMs?: number;
281
306
  askStore?: ApprovalAskStore;
282
307
  windowMarginMs?: number;
283
308
  admitMaxPerTask?: number;
284
309
  admitMaxPerOwner?: number;
310
+ /** 缺省 = 进程级单表(写侧默认同一张)。注入口只为测试与将来的多实例形。 */
311
+ governanceAskMarks?: GovernanceAskMarks;
285
312
  });
286
313
  /**
287
314
  * #151 车3 刀 3b —— design/172 §3.3 / 设计稿 §0 X-2 的**写侧准入门**。
@@ -25,8 +25,11 @@
25
25
  * grant. This is the cheapest mid-task implementation; a mode SWITCH (default → acceptEdits) still goes through the
26
26
  * shell's intent on the next task ([820] table).
27
27
  *
28
- * 🔒 FAIL-CLOSED everywhere (the inverse of question's headless default — an unanswered QUESTION lets the model
29
- * proceed; an unanswered APPROVAL must NOT let the write proceed): TTL expiry false; run abort / client
28
+ * 🔒 FAIL-CLOSED everywhere (the inverse of the question lane — an unanswered QUESTION never blocks the run: since
29
+ * #166 the coordinator reports `unavailable` and CORE decides the landing, which depends on WHEN the human became
30
+ * unreachable — at policy time the durable gate says `ask` and the leg parks; once the tool is already running core is
31
+ * past `suspendAsk`, so it continues on `declined_unavailable`; an unanswered APPROVAL must NOT let the write
32
+ * proceed): TTL expiry ⇒ false; run abort / client
30
33
  * disconnect ⇒ false. Since [879] G1 (core 1.295 OnAsk 三值化) the "no reachable human" class returns
31
34
  * "unavailable" instead of false — no ALS context AND no bound-closure broker hit(1.258 起 bg/嵌套子代经
32
35
  * spec.onAsk 的 bound 闭包 + streams broker 直达宿主活流,「无 ctx」只剩 durable-submit/headless resume
@@ -46,14 +49,21 @@ import { redactDeep, redactSecrets } from "./trace/redact.js";
46
49
  import { createLogger } from "./observability/logger.js";
47
50
  import { deriveAskId, deriveBatchId } from "./approval-ask-machine.js";
48
51
  import { ApprovalCardEnvelopeSchema, buildApprovalCard, buildApprovalCardEnvelope, buildApprovalRequestFrame, buildRevokeFrame, } from "./approval-card.js";
52
+ import { governanceAskMarksFor, runWithGovernanceAskScope } from "./governance-ask-marks.js";
49
53
  /** #151 车2:本模块自有的日志出口——同 config-provider.ts/runtime-caps-resolver.ts 先例(协调器不走
50
54
  * DI logger,构造签名是设计定稿钉死的三键 options bag,加第四个 logger 键属于重议已裁事项)。仅用于
51
55
  * D5 的一次性 store-故障 warn。 */
52
56
  const defaultLogger = createLogger();
53
57
  /** Size bound on the redacted args payload in a `tool_approval` frame (parity with question's MAX_QUESTIONS_BYTES).
54
- * Over the cap ⇒ the frame still goes out WITHOUT args (`argsOmitted: true`) unlike a question (undisplayable
55
- * headless default is safe), an approval must still reach the human: toolName+message suffice to decide, and the
56
- * fail-safe decision on blind args is the human's No. */
58
+ * Over the cap ⇒ the frame still goes out WITHOUT args (`argsOmitted: true`). The sibling lane makes the OPPOSITE
59
+ * call and that asymmetry is deliberate: an over-cap question is DROPPED and the ask reports `unavailable`
60
+ * (`question.ts` `MAX_QUESTIONS_BYTES`; since #166 that is an honest "nobody answered" and CORE picks the landing
61
+ * here the tool is already running, so core is past `suspendAsk` and continues on `declined_unavailable` rather than
62
+ * parking). Dropping an approval would land it on
63
+ * this module's "no reachable human" class instead — `"unavailable"`, which durable-parks where a checkpoint store
64
+ * exists and fail-closed DENIES where it does not (see the module header). Both of those postpone or refuse the
65
+ * write; keeping the card on the wire without args is the only option that still buys an IMMEDIATE human decision —
66
+ * toolName+message suffice to decide, and the fail-safe decision on blind args is the human's No. */
57
67
  const MAX_APPROVAL_ARGS_BYTES = 16384;
58
68
  /** How long an unanswered approval waits before it DENIES (fail-closed — the human walked away; don't hold core). */
59
69
  const DEFAULT_APPROVAL_TTL_MS = 5 * 60_000;
@@ -325,7 +335,12 @@ export class ToolApprovalCoordinator {
325
335
  /** #151 车2(D5 一次性 warn 节流):同实例只报第一次,后续只计数(避免 store 抖动期间刷屏)。 */
326
336
  storeErrorWarned = false;
327
337
  storeErrorTally = 0;
338
+ /** [2942]/[2943] `governanceForced` 的**读侧**表(写侧 = runtime-governance 的两只观察器)。
339
+ * 缺席(生产形)⇒ 每条 run 腿由 {@link runWithContext} 现铸一张、经 ALS 与写侧共享;在场 ⇒ 测试注入的
340
+ * 固定表(此时不进 ALS 作用域,读写都走这一张)。作用域理由见 `governance-ask-marks.ts` 顶注。 */
341
+ governanceAskMarks;
328
342
  constructor(opts) {
343
+ this.governanceAskMarks = opts?.governanceAskMarks;
329
344
  this.ttlMs = opts?.ttlMs ?? DEFAULT_APPROVAL_TTL_MS;
330
345
  this.askStore = opts?.askStore;
331
346
  this.windowMarginMs = opts?.windowMarginMs ?? DEFAULT_WINDOW_MARGIN_MS;
@@ -562,7 +577,13 @@ export class ToolApprovalCoordinator {
562
577
  }
563
578
  const liveSet = set;
564
579
  liveSet.add(ctx);
565
- return this.als.run(ctx, async () => {
580
+ // [2942]/[2943](codex round3/round5 两轮的修形):**写侧**的治理标作用域与审批 ctx 同拍进出,分格键
581
+ // 就是上面这把 broker `key` —— 读侧(askBroadcast)按**出处**显式算出同一把,于是委派/长命 bg 子代的
582
+ // ask 即便在另一条腿的上下文里发出也查得到自己的标(round5 的红先用例)。分格本身封的是 round3:
583
+ // `toolCallId` 是提供方给的、并非全局唯一,不分格会跨任务/跨租户串味(理由全文见 governance-ask-marks.ts)。
584
+ // 注入了固定表的实例(测试形)不再进作用域:读写都走那一张,行为与注入前逐字一致。
585
+ const enter = (body) => (this.governanceAskMarks ? body() : runWithGovernanceAskScope(key, body));
586
+ return enter(() => this.als.run(ctx, async () => {
566
587
  try {
567
588
  return await fn();
568
589
  }
@@ -580,7 +601,7 @@ export class ToolApprovalCoordinator {
580
601
  p.onTargetGone?.(ctx);
581
602
  }
582
603
  }
583
- });
604
+ }));
584
605
  }
585
606
  /** 测试/可观测性钩子:该 (owner, session/taskId) broker key 下当前活跃连接数——断言多连接注册/清扫
586
607
  * 行为时用,免得伸手进私有内部状态。 */
@@ -608,6 +629,12 @@ export class ToolApprovalCoordinator {
608
629
  * emit 时经 broker 取该 (owner, session) 的「当前」活跃流(宿主重连/续聊的新 SSE 自动接卡;
609
630
  * internalsSnapshot 冻结的闭包因此永不携死流)。查无活流 = "unavailable"(G1 回路:durable 部署
610
631
  * park;inherited plain-ask 的 park 回路 = core RB-39①,落地前该臂 = deny,与 1.257 前行为一致)。 */
632
+ /* [2942]/[2943] 与本闭包的关系(codex round4/round5 两轮追出来的形):本闭包**铸于**
633
+ * `runWithContext` 之前(装配点 routes/tasks.ts 就是这个顺序),而委派/长命 bg 子代经 core 继承链拿到它
634
+ * 之后,可能在**宿主腿早已退场**、甚至任何审批作用域之外的上下文里把 ask 发出来。治理来源标因此
635
+ * **不读环境**:`askBroadcast` 按 `identity`(经 `origin`/`primary`)显式算出与写侧同一把 broker key 去查
636
+ * ——「出处决定判定,投递到哪条连接不参与」,与 {@link AskOriginIdentity} 顶注同源。两条腿的钉见
637
+ * test/governance-forced-signal.test.ts(「长命 bg 子代形」与「脱钩定时器形」,改形前后者是红的)。 */
611
638
  boundAsk = (identity) => {
612
639
  return (req, signal) => {
613
640
  const set = this.streams.get(this.streamKey(identity.owner, identity.sessionId, identity.taskId));
@@ -694,6 +721,11 @@ export class ToolApprovalCoordinator {
694
721
  }
695
722
  : {}),
696
723
  message: typeof req.message === "string" ? redactDeep(req.message) : "",
724
+ // [2942]/[2943]:治理来源标(additive,只在为真时在场——缺席绝不编 false,见字段顶注)。
725
+ // 读的是**非消费式** peek:同一 toolCallId 的重播/failover 再入必须拿到同一份判定。
726
+ ...((this.governanceAskMarks ?? governanceAskMarksFor(this.streamKey(primary.owner, primary.sessionId, origin.taskId))).isMarked(req.toolCallId)
727
+ ? { governanceForced: true }
728
+ : {}),
697
729
  ...(bounded.omitted ? { argsOmitted: true } : { args: bounded.args }),
698
730
  };
699
731
  // #151 车2(design/172 §3.3 D3 窗长三元):有效窗——legDeadlineMonotonic 缺席时退化成 ttlMs(D1 零
@@ -732,7 +764,8 @@ export class ToolApprovalCoordinator {
732
764
  // R4-1 的三件套(见下方 ensureAsk 调用点注):迟到落盘的行 id / 本地是否已放弃 / 放弃时的收尾动作。
733
765
  let lateEnsuredAskId;
734
766
  let abandonedEnsure = false;
735
- /** 本次 `ensureAsk` 传给店里的 `createdAtMs` —— 兼作**这次插入的归属凭据**(R2-2,见 onLateSuccess)。 */
767
+ /** 本次 `ensureAsk` 传给店里的 `createdAtMs`(纯时间戳;归属判别已改由店给的 `inserted` 位回答,
768
+ * 见 `EnsureAskResult` 顶注与下方 onLateSuccess)。 */
736
769
  const attemptCreatedAtMs = Date.now();
737
770
  const voidAbandonedRow = (orphanAskId) => {
738
771
  const store = this.askStore;
@@ -788,15 +821,26 @@ export class ToolApprovalCoordinator {
788
821
  expiresAtMs,
789
822
  createdAtMs: attemptCreatedAtMs,
790
823
  }), "ensureAsk", DURABLE_CALL_TIMEOUT_MS, (late) => {
791
- // 🔴 codex 交叉复审 round2 R2-2(2026-08-06 真 finding):`ensureAsk` 是**幂等 upsert**,它迟到
792
- // 返回的那一行可能根本不是本次插的 —— 而是一条**早已存在、且正被另一次调用持有**的行(askId 是
793
- // 确定性派生,两次并发调用天然指向同一行)。旧形把任何 STREAM_PENDING 结果都当成「本次留下的
794
- // 孤儿」去收 VOID,于是本次超时放弃时会把**别人正在等的那张活卡**作废掉:真属主的 decideAsk
795
- // 随后干净地输,人的批准被吞成拒绝/延迟收敛。
796
- // 归属判据用现成数据:`createdAtMs` 是**插入者**写下的值,幂等命中时返回的是**先到者**的值。
797
- // 相等 这行是本次插的(才轮得到本次收尾);不等 幂等命中别人的行,一个字都不许动。
798
- // (残留:同一毫秒内的两次并发插入无法区分 —— ms 粒度的窄面,记在汇报存疑单。)
799
- lateEnsuredAskId = late.state === "STREAM_PENDING" && late.createdAtMs === attemptCreatedAtMs ? late.askId : undefined;
824
+ // 🔴 codex 交叉复审 round2 R2-2(2026-08-06 真 finding)+ #168 件2 根治:`ensureAsk` 是**幂等
825
+ // upsert**,它迟到返回的那一行可能根本不是本次插的 —— 而是一条**早已存在、且正被另一次调用
826
+ // 持有**的行(askId 是确定性派生,两次并发调用天然指向同一行)。把任何 STREAM_PENDING 结果都
827
+ // 当成「本次留下的孤儿」去收 VOID,会在本次超时放弃时把**别人正在等的那张活卡**作废掉:真属主
828
+ // 的 decideAsk 随后干净地输,人的批准被吞成拒绝/延迟收敛。
829
+ // 归属判据 = 店给的 `inserted` 位(引擎报的 affected 行数,`EnsureAskResult` 顶注)。R2-2 首修
830
+ // 用的 `createdAtMs === attemptCreatedAtMs` 是**猜**,在同一毫秒的两次并发插入上会给假阳性
831
+ // (那条残余随本件销账);`inserted` 是引擎对同一个问题的权威回答,严格窄于旧判据。
832
+ // `state` 复核仍留:行是本次插的,不代表此刻还没人接手(别的副本可能已经在驱动它)。
833
+ //
834
+ // 🔴 **残余,登记而不假称已解**(codex 交叉复审 2026-08-07 [high] 二,验真;**先存**,本批只把它
835
+ // 收窄没有扩大):`inserted` 回答的是「谁插的」,不是「此刻谁在驱动」。极窄的一支仍会伤人 ——
836
+ // 本次插入**已提交**、提交后那次回读慢到超时、随后**每一次**复核读也全失败(店严重退化,才会走
837
+ // `indeterminate` 臂),而这期间另一个副本幂等命中这条 STREAM_PENDING 行并开始真驱动它;此时
838
+ // 迟到结果带着 `inserted: true` 回来,会把别人正在等的卡收成 VOID。
839
+ // 旧的 `createdAtMs` 判据在同一支上同样会 VOID(那是本次自己写的时间戳)⇒ 本批不是引入方,是
840
+ // 严格缩小了误伤集合(幂等命中的行从此一个字都不碰)。
841
+ // 这条残余与 R4-1 的取舍是同一枚硬币:不收 = 留一条无属主的行被孤儿腿 park 出**幻影 gate**。
842
+ // 真解 = 给行一个 attempt 级的属主租约(新列 + CAS 带租约),属协议改动,开题上报,不在本批。
843
+ lateEnsuredAskId = late.inserted && late.row.state === "STREAM_PENDING" ? late.row.askId : undefined;
800
844
  if (abandonedEnsure && lateEnsuredAskId !== undefined)
801
845
  voidAbandonedRow(lateEnsuredAskId);
802
846
  });
@@ -808,9 +852,9 @@ export class ToolApprovalCoordinator {
808
852
  // 是**过去**某次早已返回的 askBroadcast 调用,进程里已经没有任何存活的闭包会再驱动它 settle,
809
853
  // TTL 到点时 expireAsk 对一条非 STREAM_PENDING 行同样只会干净地输,一样什么都不做——挂死到进程
810
854
  // 重启。修法:行不是 STREAM_PENDING ⇒ 不重新挂卡,直接回放既有终局(不落 pending、不 emit)。
811
- if (existingOrCreated.state !== "STREAM_PENDING") {
855
+ if (existingOrCreated.row.state !== "STREAM_PENDING") {
812
856
  releaseAdmission(); // X-2:回放既有终局 = 这只 ask 从未占用一条未决腿,名额立刻还回去
813
- return replayTerminalAskRow(existingOrCreated);
857
+ return replayTerminalAskRow(existingOrCreated.row);
814
858
  }
815
859
  // 🔴 F2 修:行 = 真源。刚插的行上这三样与本地一致(采信是恒等操作);**重入**拿到既有行时,
816
860
  // 采信才是唯一正确的做法 —— 帧、定时器、落库三处从此只有一个 deadline、一个 approvalId、一份卡。
@@ -820,17 +864,30 @@ export class ToolApprovalCoordinator {
820
864
  // 跳过),而车4 的回决端点按 askId 仍决得动它:人有可能照着一份**不是行上那份**的卡做授权。
821
865
  // 采信行上一个**无界/非有限**的 deadline 同样危险:它会把 pending 条目与准入名额钉在那里远超配置窗。
822
866
  // 裁:两样任一不合 ⇒ 不编造替身、不发帧,**fail-safe 走 park**(行本身交给对账收敛器处理)。
823
- const persistedEnvelope = ApprovalCardEnvelopeSchema.safeParse(existingOrCreated.cardJson);
824
- const sanePersistedDeadline = Number.isFinite(existingOrCreated.expiresAtMs) && existingOrCreated.expiresAtMs <= Date.now() + effectiveWindowMs + DURABLE_CALL_TIMEOUT_MS;
867
+ const persistedEnvelope = ApprovalCardEnvelopeSchema.safeParse(existingOrCreated.row.cardJson);
868
+ const sanePersistedDeadline = Number.isFinite(existingOrCreated.row.expiresAtMs) && existingOrCreated.row.expiresAtMs <= Date.now() + effectiveWindowMs + DURABLE_CALL_TIMEOUT_MS;
825
869
  if (!persistedEnvelope.success || !sanePersistedDeadline) {
826
- this.noteStoreError(new Error(`persisted approval row is unusable (envelopeOk=${persistedEnvelope.success}, deadlineOk=${sanePersistedDeadline})`), "ensureAsk(persisted-row-unusable)");
870
+ // #168 件2:`inserted` 让这条臂能分清两种成因 —— 幂等命中的**别人的**行不合形(存量/旧版/坏行),
871
+ // 与「本次刚写下的行自己读不回来」(店在改写我们的字节,或本地铸形与 schema 不一致)。处置相同
872
+ // (都不编造替身、都走 park),但后者是本进程自己的账,归因不许并成前者。
873
+ this.noteStoreError(new Error(`persisted approval row is unusable (envelopeOk=${persistedEnvelope.success}, deadlineOk=${sanePersistedDeadline}, ` +
874
+ `origin=${existingOrCreated.inserted ? "row-this-call-just-inserted" : "row-hit-idempotently"})`), "ensureAsk(persisted-row-unusable)");
827
875
  releaseAdmission();
828
876
  return "unavailable"; // park 路由;绝不用一个编造的身份把这张卡推上 wire
829
877
  }
830
- persistedExpiresAtMs = existingOrCreated.expiresAtMs;
878
+ persistedExpiresAtMs = existingOrCreated.row.expiresAtMs;
831
879
  wireApprovalId = persistedEnvelope.data.approvalId;
832
880
  frame.approvalId = wireApprovalId; // 旧帧的去重键必须与新帧同值(两帧同带 approvalId 是消费端的对账口)
833
881
  cardForFrame = persistedEnvelope.data.card;
882
+ // 🔴 [2942] codex 交叉复审 round1 [high](验真):`governanceForced` 也归入「行 = 真源」的 F2 家族。
883
+ // 本地判据(`governanceAskMarks`)是**进程内**表:它不跨副本、不跨重启。幂等命中一条既有行时
884
+ // (failover / 重启后重入),本地表恒空而行上的卡里存着它**落库那一刻**的判定 —— 两族帧若各读各的,
885
+ // 同一个 `approvalId` 会带着**互相矛盾**的出处上 wire(旧帧无键、呈卡帧 true,反向亦然)。
886
+ // 与 approvalId/deadline 同一条裁定:采信行。缺席也要**如实同步**(删键,不是留着本地的旧真值)。
887
+ if (cardForFrame.governanceForced === true)
888
+ frame.governanceForced = true;
889
+ else
890
+ delete frame.governanceForced;
834
891
  }
835
892
  catch (err) {
836
893
  this.noteStoreError(err, "ensureAsk");
@@ -15,21 +15,35 @@
15
15
  * 维护:core 新键到来时,transparently 透传的加进对应 PROJECTED 并去实现投影;有意不上 wire 的加进
16
16
  * EXCLUDED 并写一行理由。谁改投影(project.ts / fleet-bus.ts / roster-store-sql.ts)谁同步本文件。
17
17
  */
18
- import type { TaskNotificationPayload, BackgroundChildEvent, RosterEntry, AskRequest, TaskEvent } from "@sema-agent/core";
18
+ import type { TaskNotificationPayload, BackgroundChildEvent, RosterEntry, AskRequest, TaskEvent, MailboxMessage, MailboxStore } from "@sema-agent/core";
19
19
  /** 编译期断言:T 必须收敛到 never(有残余键 = tsc 红)。 */
20
20
  type AssertAllKeysHandled<T extends never> = T;
21
21
  type NotificationProjected = "task_id" | "task_type" | "toolUseId" | "status" | "summary" | "result" | "output_file" | "usage" | "sessionId" | "seq" | "lines" | "stoppedBy" | "source" | "exitCode" | "partial" | "diagnostics" | "recentSteps" | "editedFiles" | "resumable" | "completionId" | "error" | "errorCode";
22
- type _GuardNotification = AssertAllKeysHandled<Exclude<keyof TaskNotificationPayload, NotificationProjected>>;
22
+ type NotificationExcluded = "peer";
23
+ type _GuardNotification = AssertAllKeysHandled<Exclude<keyof TaskNotificationPayload, NotificationProjected | NotificationExcluded>>;
23
24
  type BgNotifProjected = "taskId" | "sessionId" | "seq" | "status" | "summary" | "stoppedBy" | "resumable" | "recentSteps" | "editedFiles" | "usage" | "transcriptId" | "rootSessionId" | "parentTaskId" | "parentToolCallId" | "completionId";
24
25
  type BgNotifExcluded = "kind" | "sessionScoped" | "owner" | "scope" | "description" | "agentType" | "name" | "currentAction" | "currentTool" | "parentSessionId" | "startedAt" | "workflowRunId" | "progressTaskId" | "progressParentTaskId";
25
26
  type _GuardBgNotif = AssertAllKeysHandled<Exclude<keyof BackgroundChildEvent, BgNotifProjected | BgNotifExcluded>>;
26
27
  type RosterProjected = "name" | "agentId" | "sessionId" | "toolUseId" | "owner" | "scope" | "sessionScoped" | "rootSessionId" | "model" | "modelFallback" | "createdAt";
27
28
  type _GuardRoster = AssertAllKeysHandled<Exclude<keyof RosterEntry, RosterProjected>>;
28
29
  type AskProjected = "toolName" | "toolCallId" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName" | "delegation";
29
- type AskExcluded = "preview" | "principal" | "requiresRealApproval" | "riskAxes" | "boundInputHash";
30
+ type AskExcluded = "preview" | "principal" | "requiresRealApproval" | "riskAxes" | "boundInputHash" | "ruleSuggestions";
30
31
  type _GuardAsk = AssertAllKeysHandled<Exclude<keyof AskRequest, AskProjected | AskExcluded>>;
31
32
  type TaskEventHandled = "text_delta" | "reasoning_delta" | "tool_start" | "tool_end" | "turn_end" | "compacted" | "diagnostics" | "message_committed" | "status" | "task_notification" | "task_progress" | "steering_injected" | "workspace_changed" | "done" | "context_usage" | "compaction_outcome" | "human_input" | "wiring_manifest";
32
33
  type _GuardTaskEvent = AssertAllKeysHandled<Exclude<TaskEvent["type"], TaskEventHandled>>;
33
- export type CoreKeysetGuards = [_GuardNotification, _GuardBgNotif, _GuardRoster, _GuardAsk, _GuardTaskEvent];
34
+ type MailboxProjected = "seq" | "from" | "content" | "sentAt" | "hopChain";
35
+ type _GuardMailbox = AssertAllKeysHandled<Exclude<keyof MailboxMessage, MailboxProjected>>;
36
+ type MailboxAppendMessage = Parameters<MailboxStore["append"]>[2];
37
+ type MailboxAppendProjected = "from" | "content" | "sentAt" | "hopChain";
38
+ type _GuardMailboxAppend = AssertAllKeysHandled<Exclude<keyof MailboxAppendMessage, MailboxAppendProjected>>;
39
+ export type CoreKeysetGuards = [
40
+ _GuardNotification,
41
+ _GuardBgNotif,
42
+ _GuardRoster,
43
+ _GuardAsk,
44
+ _GuardTaskEvent,
45
+ _GuardMailbox,
46
+ _GuardMailboxAppend
47
+ ];
34
48
  export {};
35
49
  //# sourceMappingURL=core-keyset-guard.d.ts.map
@@ -12,7 +12,7 @@
12
12
  * old session-join `toolOutput` hook is gone, there's a real source now); `eventId`/`parentToolCallId` (§E2) ride
13
13
  * each content event, additive / tolerate-absent.
14
14
  */
15
- import type { TaskResult } from "@sema-agent/core";
15
+ import type { ApprovalSettledBy, TaskResult } from "@sema-agent/core";
16
16
  import type { RunEvent, RunRecord } from "../plugins/store-contracts.js";
17
17
  import type { ModelUsageDelta, ModelUsageTracker, PromptManifestRecord } from "../budget.js";
18
18
  type AppendFn = (type: string, data: unknown) => Promise<void>;
@@ -90,6 +90,7 @@ export type TraceBlock = {
90
90
  totalChars?: number;
91
91
  structured?: unknown;
92
92
  errorCode?: string;
93
+ settledBy?: ApprovalSettledBy;
93
94
  eventId?: string;
94
95
  parentToolCallId?: string;
95
96
  } | {
@@ -131,6 +132,7 @@ export declare function toolEndEventData(ev: {
131
132
  totalChars?: number;
132
133
  structured?: unknown;
133
134
  errorCode?: string;
135
+ settledBy?: ApprovalSettledBy;
134
136
  eventId?: string;
135
137
  parentToolCallId?: string;
136
138
  }): Record<string, unknown>;
@@ -284,6 +286,13 @@ export declare function workspaceChangedEventData(ev: {
284
286
  * · `interaction.posture`(三值:interactive / headless / absent)—— **裁:租户可见**。它是部署形的
285
287
  * 自述(这条腿有没有人能答问题),消费端据此决定渲不渲交互面;不是治理秘密,也不构成任何治理位的
286
288
  * 侧信道(它不参与 governance 那 4 个布尔的还原)。
289
+ * · `permissionRules.storeWired`(布尔;core 5.18.0 design/179 新段)—— **裁:租户可见**。同族判据:
290
+ * 它与 `parkLane.capable` / `elicit.seamWired` / `fleet.backgroundAgentStore` 是同一类「这条腿接没接
291
+ * 某个可选设施」的部署形自述,而那三个早已租户可见。消费端的真需求也在租户侧:审批卡要不要渲染
292
+ * 「不再询问」这一格,取决于这台 worker 有没有规则店 —— 剥掉它,客户端只能靠试(提交后被拒)才知道。
293
+ * 它**不在** `governance` 段里(core 自己把它放在段外),也不构成那 4 个治理布尔的侧信道。
294
+ * ⚠️ 恒在段(core 侧 `permissionRules` 无 `?`):本仓未接规则店 ⇒ `storeWired: false`,这是**真值**
295
+ * 不是缺席 —— 消费端读到 false 就是「这台 worker 没有这条车道」,不许当成「这版引擎还没这个概念」。
287
296
  * 未来 core 加一段而它没有 audience 标签时,**先在这里补一条裁定再决定挑不挑键** —— 段级完备性钉
288
297
  * (test/wiring-manifest-projection.test.ts)会在那一刻先把人拦下来。
289
298
  */
@@ -1,3 +1,6 @@
1
+ // 值导入(非 `import type`):`settledBy` 的闭集判据必须**复用 core 的谓词**——本仓抄一份三元词表
2
+ // 就是值拷贝漂移源(上游加员时两处静默不同步)。core 5.18.1 #187 导出的单一属主。
3
+ import { isApprovalSettledBy } from "@sema-agent/core";
1
4
  import { redactSecrets, redactDeep } from "./redact.js";
2
5
  /** E8 (shell-host): drain the per-task model-usage accumulator into an append-only `model_usage` DELTA event.
3
6
  * Shared by BOTH durable-write legs (runInBackground + the resume leg) so the persisted shape never drifts; the
@@ -163,6 +166,16 @@ export function toolEndEventData(ev) {
163
166
  // 带 default 臂,禁按两值穷举。verbatim 依据:码是机器短串标识符、非用户内容(不脱敏),边界靠
164
167
  // 显式上限而非词表 —— >128 字符=非法形,整键丢弃(不截断:截断会铸出 core 从未发过的码)。
165
168
  ...(typeof ev.errorCode === "string" && ev.errorCode.length <= 128 ? { errorCode: ev.errorCode } : {}),
169
+ // core 5.18.1 #187([3110] 提货批):`settledBy` = 这一次调用的**结算来源**("human"/"timeout"/
170
+ // "aborted"),只挂在经审批结算的那个 tool_end 上。与上面的 errorCode **不同族**,两条差别都承重:
171
+ // ① 它是**闭集**,判据走 core 导出的 `isApprovalSettledBy`(单一属主)—— 不在本仓抄词表,也不拿
172
+ // 长度上限当边界(那是开集的兜底姿势,对闭集等于没门:出格词照样上 wire,下游按闭集写的
173
+ // switch 会撞 default 或更坏地把它当成一次真结算)。上游加员时判据自动跟随。
174
+ // ② **缺席不是语义**:core 契约逐字写「Absence = an older caller — consumers must not infer
175
+ // semantics from a missing key」;姿态臂(headless auto-deny / 整体放行)本就不填。所以缺席
176
+ // 一律不铸键 —— 既不折 "human" 也不折 null,把「不知道」如实表达成「没有这个键」。
177
+ // 值是引擎铸的枚举短串、非用户内容 ⇒ 不脱敏。
178
+ ...(isApprovalSettledBy(ev.settledBy) ? { settledBy: ev.settledBy } : {}),
166
179
  ...identityFields(ev),
167
180
  };
168
181
  }
@@ -363,6 +376,13 @@ export function workspaceChangedEventData(ev) {
363
376
  * · `interaction.posture`(三值:interactive / headless / absent)—— **裁:租户可见**。它是部署形的
364
377
  * 自述(这条腿有没有人能答问题),消费端据此决定渲不渲交互面;不是治理秘密,也不构成任何治理位的
365
378
  * 侧信道(它不参与 governance 那 4 个布尔的还原)。
379
+ * · `permissionRules.storeWired`(布尔;core 5.18.0 design/179 新段)—— **裁:租户可见**。同族判据:
380
+ * 它与 `parkLane.capable` / `elicit.seamWired` / `fleet.backgroundAgentStore` 是同一类「这条腿接没接
381
+ * 某个可选设施」的部署形自述,而那三个早已租户可见。消费端的真需求也在租户侧:审批卡要不要渲染
382
+ * 「不再询问」这一格,取决于这台 worker 有没有规则店 —— 剥掉它,客户端只能靠试(提交后被拒)才知道。
383
+ * 它**不在** `governance` 段里(core 自己把它放在段外),也不构成那 4 个治理布尔的侧信道。
384
+ * ⚠️ 恒在段(core 侧 `permissionRules` 无 `?`):本仓未接规则店 ⇒ `storeWired: false`,这是**真值**
385
+ * 不是缺席 —— 消费端读到 false 就是「这台 worker 没有这条车道」,不许当成「这版引擎还没这个概念」。
366
386
  * 未来 core 加一段而它没有 audience 标签时,**先在这里补一条裁定再决定挑不挑键** —— 段级完备性钉
367
387
  * (test/wiring-manifest-projection.test.ts)会在那一刻先把人拦下来。
368
388
  */
@@ -386,6 +406,7 @@ export function wiringManifestEventData(ev) {
386
406
  const fleetIn = sec(m.fleet) ?? {};
387
407
  const legIn = sec(m.leg) ?? {};
388
408
  const interactionIn = sec(m.interaction) ?? {};
409
+ const rulesIn = sec(m.permissionRules) ?? {};
389
410
  const ask = nonEmpty({
390
411
  ...(str(askIn.form) !== undefined ? { form: askIn.form } : {}),
391
412
  ...(str(askIn.provenance) !== undefined ? { provenance: askIn.provenance } : {}),
@@ -413,6 +434,11 @@ export function wiringManifestEventData(ev) {
413
434
  ...(bool(fleetIn.backgroundAgentStore) !== undefined ? { backgroundAgentStore: fleetIn.backgroundAgentStore } : {}),
414
435
  ...(bool(fleetIn.hostChildEventSink) !== undefined ? { hostChildEventSink: fleetIn.hostChildEventSink } : {}),
415
436
  });
437
+ // core 5.18.0 design/179 段(裁定见头注:租户可见)。逐键挑,与其余段同规:非布尔按缺席处理
438
+ // (不铸假值——一个编出来的 `false` 会被读成「确实没接规则店」,而真相是这段读不出来)。
439
+ const permissionRules = nonEmpty({
440
+ ...(bool(rulesIn.storeWired) !== undefined ? { storeWired: rulesIn.storeWired } : {}),
441
+ });
416
442
  return {
417
443
  ...(num(m.schemaVersion) !== undefined ? { schemaVersion: m.schemaVersion } : {}),
418
444
  ...(str(legIn.kind) !== undefined ? { leg: { kind: legIn.kind } } : {}),
@@ -423,6 +449,7 @@ export function wiringManifestEventData(ev) {
423
449
  ...(parkLane !== undefined ? { parkLane } : {}),
424
450
  ...(str(sessionIn.store) !== undefined ? { session: { store: sessionIn.store } } : {}),
425
451
  ...(fleet !== undefined ? { fleet } : {}),
452
+ ...(permissionRules !== undefined ? { permissionRules } : {}),
426
453
  // governance —— **刻意不在此**(见头注:受众标 operator,无投影不披露)。
427
454
  // configFingerprint —— **也刻意不在此**(codex 复审 2026-08-06 F1,验真后采纳)。剥掉一个段却把
428
455
  // **对它取的哈希**发出去,等于没剥:core 的算法是
@@ -685,6 +712,10 @@ export function toolResultFieldsOf(d) {
685
712
  // core 5.9.0 W3([2535]):机器可判别的 park/死投影码(`gate.parked`/`tool.not_found`)——写侧
686
713
  // toolEndEventData 已带,读腿必须同键 verbatim(absent⇒absent),否则冷回放退回锚 abort 文案字面。
687
714
  ...(typeof d.errorCode === "string" ? { errorCode: d.errorCode } : {}),
715
+ // core 5.18.1 #187:结算来源(闭集)——写侧 toolEndEventData 已带,读腿同键 verbatim(absent⇒absent)。
716
+ // 🔴 读腿的闭集门是**独立的一道**,不是写侧那道的复述:账本行可以来自任何引擎世代、任何第三方
717
+ // producer,写侧的门管不到它们;这里放行 = 把一个 core 从未发过的词直接送上 wire。
718
+ ...(isApprovalSettledBy(d.settledBy) ? { settledBy: d.settledBy } : {}),
688
719
  ...identityFields(d),
689
720
  };
690
721
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.4.0",
3
+ "version": "7.6.0",
4
4
  "description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -54,7 +54,7 @@
54
54
  "build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
55
55
  },
56
56
  "dependencies": {
57
- "@sema-agent/core": "^5.16.0",
57
+ "@sema-agent/core": "^5.19.0",
58
58
  "@sema-agent/registry-core": "^0.16.0",
59
59
  "e2b": "^2.28.0",
60
60
  "libsodium-wrappers": "^0.8.4",
@@ -69,7 +69,7 @@
69
69
  "sharp": "^0.35.3"
70
70
  },
71
71
  "devDependencies": {
72
- "@sema-agent/sdk": "^6.7.0",
72
+ "@sema-agent/sdk": "^6.9.0",
73
73
  "@types/libsodium-wrappers": "^0.7.14",
74
74
  "@types/node": "22.10.2",
75
75
  "@types/pg": "^8.20.0",
@@ -1,10 +0,0 @@
1
- import { FileError, StubExecutionEnv, type Result } from "@sema-agent/core";
2
- /**
3
- * 无文件系统的路径裁决 env(见文件头)。只覆写 `absolutePath` 一面,其余全部继承 `StubExecutionEnv`
4
- * 的 `not_supported` ——**继承而非逐一手写**是刻意的:core 日后给 `ExecutionEnv` 加必填面时,新面会
5
- * 随 `StubExecutionEnv` 一起到位并保持同一个诚实答案,不会在这里留下一个悄悄编出来的假答案。
6
- */
7
- export declare class LexicalPathExecutionEnv extends StubExecutionEnv {
8
- absolutePath(path: string): Promise<Result<string, FileError>>;
9
- }
10
- //# sourceMappingURL=lexical-path-env.d.ts.map