@sema-agent/server 7.3.0 → 7.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/USAGE.md +63 -0
  2. package/dist/approval-card.d.ts +15 -3
  3. package/dist/approval-card.js +41 -7
  4. package/dist/approval-reconciler.d.ts +120 -16
  5. package/dist/approval-reconciler.js +146 -19
  6. package/dist/boot/coordinators.js +13 -3
  7. package/dist/boot/deferred-sandbox-path-env.d.ts +99 -0
  8. package/dist/boot/deferred-sandbox-path-env.js +279 -0
  9. package/dist/boot/execution-env.js +11 -1
  10. package/dist/boot/org-memory.d.ts +6 -0
  11. package/dist/boot/org-memory.js +1 -1
  12. package/dist/boot/reapers.d.ts +2 -0
  13. package/dist/boot/reapers.js +11 -4
  14. package/dist/boot/resolve-spec.d.ts +3 -2
  15. package/dist/boot/resolve-spec.js +175 -63
  16. package/dist/boot/runner-deps.d.ts +23 -1
  17. package/dist/boot/runner-deps.js +8 -11
  18. package/dist/boot/workflow-orchestration.d.ts +8 -3
  19. package/dist/boot/workflow-orchestration.js +23 -1
  20. package/dist/capabilities/center-prompts.js +4 -1
  21. package/dist/config-center/apply-effective.js +33 -10
  22. package/dist/config-types.d.ts +32 -9
  23. package/dist/config.d.ts +6 -1
  24. package/dist/config.js +65 -12
  25. package/dist/elicitation.js +5 -1
  26. package/dist/env-facts.d.ts +3 -1
  27. package/dist/env-facts.js +3 -1
  28. package/dist/fleet/fleet-bus.d.ts +6 -1
  29. package/dist/fleet/fleet-bus.js +25 -3
  30. package/dist/governance-ask-marks.d.ts +31 -0
  31. package/dist/governance-ask-marks.js +122 -0
  32. package/dist/hooks/hook-runner.d.ts +28 -0
  33. package/dist/hooks/hook-runner.js +180 -24
  34. package/dist/http/routes/diagnostics.d.ts +84 -0
  35. package/dist/http/routes/diagnostics.js +145 -0
  36. package/dist/http/routes/memory-policy.d.ts +2 -1
  37. package/dist/http/routes/memory-policy.js +77 -13
  38. package/dist/http/routes/runs.js +1 -1
  39. package/dist/http/routes/tasks.js +87 -29
  40. package/dist/http/server.d.ts +10 -0
  41. package/dist/http/server.js +29 -12
  42. package/dist/http/wire-types.d.ts +7 -2
  43. package/dist/main.js +51 -8
  44. package/dist/observability/fail-open.d.ts +109 -0
  45. package/dist/observability/fail-open.js +227 -0
  46. package/dist/observability/prompt-manifest.d.ts +17 -0
  47. package/dist/observability/prompt-manifest.js +8 -0
  48. package/dist/orchestration/workflow-notify-journal.d.ts +57 -1
  49. package/dist/orchestration/workflow-notify-journal.js +137 -32
  50. package/dist/parked-decide.js +9 -4
  51. package/dist/plugins/approval-ask-store-memory.d.ts +2 -2
  52. package/dist/plugins/approval-ask-store-memory.js +3 -2
  53. package/dist/plugins/approval-ask-store-sql.d.ts +27 -5
  54. package/dist/plugins/approval-ask-store-sql.js +9 -2
  55. package/dist/plugins/background-shell-support.d.ts +1 -1
  56. package/dist/plugins/background-shell-support.js +2 -2
  57. package/dist/plugins/checkpoint-store-sql.d.ts +62 -6
  58. package/dist/plugins/checkpoint-store-sql.js +71 -11
  59. package/dist/plugins/local-checkpoint-store.d.ts +20 -1
  60. package/dist/plugins/local-checkpoint-store.js +19 -0
  61. package/dist/plugins/mailbox-store-sql.d.ts +4 -10
  62. package/dist/plugins/mailbox-store-sql.js +57 -4
  63. package/dist/question.d.ts +18 -14
  64. package/dist/question.js +83 -34
  65. package/dist/runs.d.ts +8 -0
  66. package/dist/runs.js +15 -2
  67. package/dist/runtime-governance.d.ts +18 -0
  68. package/dist/runtime-governance.js +90 -3
  69. package/dist/task-settings.d.ts +16 -21
  70. package/dist/task-settings.js +22 -19
  71. package/dist/tool-approval.d.ts +33 -6
  72. package/dist/tool-approval.js +95 -30
  73. package/dist/trace/core-keyset-guard.d.ts +17 -3
  74. package/dist/trace/project.d.ts +36 -1
  75. package/dist/trace/project.js +55 -2
  76. package/package.json +3 -3
  77. package/dist/boot/lexical-path-env.d.ts +0 -14
  78. package/dist/boot/lexical-path-env.js +0 -116
@@ -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;
@@ -156,10 +166,18 @@ function boundArgs(args) {
156
166
  }
157
167
  /** #151 车3 刀 3b:`AskRequest.boundInputHash` 的**边界窄读**(车5 §9 C2 的对账 join 键)。
158
168
  *
159
- * 🔴 树上的 core d.ts(5.13.0)**没有这个键**(全仓 0 命中),字段由 core 认领、随预发/终版到货。
160
- * 窄读让编译与行为解耦:今天恒回 `null`(= 该行结构上永不满足收敛器判据 1,`unmatchableNoHash` 响亮化,
161
- * 这是设计要的 fail-safe),core 供值后同一行代码自然点亮。非字符串/空串一律按缺席处置 —— 一个形不对的
162
- * hash 比没有 hash 更危险(它会让「硬相等」这道门在一个垃圾值上偶然成立)。 */
169
+ * **已点亮**(#164 翻真验证车,2026-08-07 亲验 core 5.15.0):`AskRequest.boundInputHash` 现在既在类型面
170
+ * (`core/tool-policy.d.ts:83`,`readonly boundInputHash?: string`)也在真码面(`core/tool-policy.js:610`:
171
+ * `onAsk({ ...req, boundInputHash: boundInputHashOf(presented.value), args: approverView.value })`)——
172
+ * 每一只经 `resolveAsk` ask 都带值。窄读当初就是为这一刻写的:**同一行代码**不改而自然点亮,收敛器
173
+ * 判据 1 从「结构上恒不命中」变成会命中,`unmatchableNoHash` 从「响亮化的盲区」退回它该有的边缘含义。
174
+ * (旧注写的「树上 core 5.13.0 没有这个键 ⇒ 今天恒回 null」自 core 5.15.0 提货起即过期,勿据以判断。)
175
+ *
176
+ * 🔴 保留窄读而不改 import 的理由不变:`boundInputHashOf` 至今**不在 core 的公开导出面**上
177
+ * (package `exports` 只有 `.` / `./bench` / `./fixtures`),server 因此既算不出也不该算这个摘要 ——
178
+ * 它的职责恒是「原样透传落列」。非字符串/空串一律按缺席处置:一个形不对的 hash 比没有 hash 更危险
179
+ * (它会让「硬相等」这道门在一个垃圾值上偶然成立)。钉:test/stream-approval-on-e2e.test.ts 件4
180
+ * (正控 / 缺席 / 空串三臂 + 收敛器 `unmatchableNoHash` 归零的非零对照)。 */
163
181
  function readBoundInputHash(req) {
164
182
  if (req === null || typeof req !== "object")
165
183
  return null;
@@ -317,7 +335,12 @@ export class ToolApprovalCoordinator {
317
335
  /** #151 车2(D5 一次性 warn 节流):同实例只报第一次,后续只计数(避免 store 抖动期间刷屏)。 */
318
336
  storeErrorWarned = false;
319
337
  storeErrorTally = 0;
338
+ /** [2942]/[2943] `governanceForced` 的**读侧**表(写侧 = runtime-governance 的两只观察器)。
339
+ * 缺席(生产形)⇒ 每条 run 腿由 {@link runWithContext} 现铸一张、经 ALS 与写侧共享;在场 ⇒ 测试注入的
340
+ * 固定表(此时不进 ALS 作用域,读写都走这一张)。作用域理由见 `governance-ask-marks.ts` 顶注。 */
341
+ governanceAskMarks;
320
342
  constructor(opts) {
343
+ this.governanceAskMarks = opts?.governanceAskMarks;
321
344
  this.ttlMs = opts?.ttlMs ?? DEFAULT_APPROVAL_TTL_MS;
322
345
  this.askStore = opts?.askStore;
323
346
  this.windowMarginMs = opts?.windowMarginMs ?? DEFAULT_WINDOW_MARGIN_MS;
@@ -554,7 +577,13 @@ export class ToolApprovalCoordinator {
554
577
  }
555
578
  const liveSet = set;
556
579
  liveSet.add(ctx);
557
- 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 () => {
558
587
  try {
559
588
  return await fn();
560
589
  }
@@ -572,7 +601,7 @@ export class ToolApprovalCoordinator {
572
601
  p.onTargetGone?.(ctx);
573
602
  }
574
603
  }
575
- });
604
+ }));
576
605
  }
577
606
  /** 测试/可观测性钩子:该 (owner, session/taskId) broker key 下当前活跃连接数——断言多连接注册/清扫
578
607
  * 行为时用,免得伸手进私有内部状态。 */
@@ -600,6 +629,12 @@ export class ToolApprovalCoordinator {
600
629
  * emit 时经 broker 取该 (owner, session) 的「当前」活跃流(宿主重连/续聊的新 SSE 自动接卡;
601
630
  * internalsSnapshot 冻结的闭包因此永不携死流)。查无活流 = "unavailable"(G1 回路:durable 部署
602
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 子代形」与「脱钩定时器形」,改形前后者是红的)。 */
603
638
  boundAsk = (identity) => {
604
639
  return (req, signal) => {
605
640
  const set = this.streams.get(this.streamKey(identity.owner, identity.sessionId, identity.taskId));
@@ -686,6 +721,11 @@ export class ToolApprovalCoordinator {
686
721
  }
687
722
  : {}),
688
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
+ : {}),
689
729
  ...(bounded.omitted ? { argsOmitted: true } : { args: bounded.args }),
690
730
  };
691
731
  // #151 车2(design/172 §3.3 D3 窗长三元):有效窗——legDeadlineMonotonic 缺席时退化成 ttlMs(D1 零
@@ -724,7 +764,8 @@ export class ToolApprovalCoordinator {
724
764
  // R4-1 的三件套(见下方 ensureAsk 调用点注):迟到落盘的行 id / 本地是否已放弃 / 放弃时的收尾动作。
725
765
  let lateEnsuredAskId;
726
766
  let abandonedEnsure = false;
727
- /** 本次 `ensureAsk` 传给店里的 `createdAtMs` —— 兼作**这次插入的归属凭据**(R2-2,见 onLateSuccess)。 */
767
+ /** 本次 `ensureAsk` 传给店里的 `createdAtMs`(纯时间戳;归属判别已改由店给的 `inserted` 位回答,
768
+ * 见 `EnsureAskResult` 顶注与下方 onLateSuccess)。 */
728
769
  const attemptCreatedAtMs = Date.now();
729
770
  const voidAbandonedRow = (orphanAskId) => {
730
771
  const store = this.askStore;
@@ -772,23 +813,34 @@ export class ToolApprovalCoordinator {
772
813
  // (未打 tag),按 SCHEMA POLICY drop-and-recreate,不给一个从未出厂的形状留兼容层(§6.2 裁定 +
773
814
  // 本仓「不做临时方案」纪律)。
774
815
  cardJson: buildApprovalCardEnvelope(id, card),
775
- // 车5 §9 C2 的对账 join 键。**窄读**(`AskRequest.boundInputHash` 在树上的 core 5.13.0 里 0 命中,
776
- // 字段由 core 认领、待 pre/终版到货)——有则存、无则 null;缺席 ⇒ 收敛器判据 1 结构上不命中
777
- // (`unmatchableNoHash` 响亮化),这是**设计要的** fail-safe,不是降级(禁「能取到时才比」)。
816
+ // 车5 §9 C2 的对账 join 键。**窄读**(顶注:core 5.15.0 起真有值,窄读的理由改为「摘要函数
817
+ // 不在 core 公开导出面,server 只透传不重算」)——有则存、无则 null;缺席 ⇒ 收敛器判据 1
818
+ // 结构上不命中(`unmatchableNoHash` 计数),这是**设计要的** fail-safe,不是降级(禁「能取到时才比」)。
778
819
  boundInputHash: readBoundInputHash(req),
779
820
  schemaVersion: 1,
780
821
  expiresAtMs,
781
822
  createdAtMs: attemptCreatedAtMs,
782
823
  }), "ensureAsk", DURABLE_CALL_TIMEOUT_MS, (late) => {
783
- // 🔴 codex 交叉复审 round2 R2-2(2026-08-06 真 finding):`ensureAsk` 是**幂等 upsert**,它迟到
784
- // 返回的那一行可能根本不是本次插的 —— 而是一条**早已存在、且正被另一次调用持有**的行(askId 是
785
- // 确定性派生,两次并发调用天然指向同一行)。旧形把任何 STREAM_PENDING 结果都当成「本次留下的
786
- // 孤儿」去收 VOID,于是本次超时放弃时会把**别人正在等的那张活卡**作废掉:真属主的 decideAsk
787
- // 随后干净地输,人的批准被吞成拒绝/延迟收敛。
788
- // 归属判据用现成数据:`createdAtMs` 是**插入者**写下的值,幂等命中时返回的是**先到者**的值。
789
- // 相等 这行是本次插的(才轮得到本次收尾);不等 幂等命中别人的行,一个字都不许动。
790
- // (残留:同一毫秒内的两次并发插入无法区分 —— ms 粒度的窄面,记在汇报存疑单。)
791
- 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;
792
844
  if (abandonedEnsure && lateEnsuredAskId !== undefined)
793
845
  voidAbandonedRow(lateEnsuredAskId);
794
846
  });
@@ -800,9 +852,9 @@ export class ToolApprovalCoordinator {
800
852
  // 是**过去**某次早已返回的 askBroadcast 调用,进程里已经没有任何存活的闭包会再驱动它 settle,
801
853
  // TTL 到点时 expireAsk 对一条非 STREAM_PENDING 行同样只会干净地输,一样什么都不做——挂死到进程
802
854
  // 重启。修法:行不是 STREAM_PENDING ⇒ 不重新挂卡,直接回放既有终局(不落 pending、不 emit)。
803
- if (existingOrCreated.state !== "STREAM_PENDING") {
855
+ if (existingOrCreated.row.state !== "STREAM_PENDING") {
804
856
  releaseAdmission(); // X-2:回放既有终局 = 这只 ask 从未占用一条未决腿,名额立刻还回去
805
- return replayTerminalAskRow(existingOrCreated);
857
+ return replayTerminalAskRow(existingOrCreated.row);
806
858
  }
807
859
  // 🔴 F2 修:行 = 真源。刚插的行上这三样与本地一致(采信是恒等操作);**重入**拿到既有行时,
808
860
  // 采信才是唯一正确的做法 —— 帧、定时器、落库三处从此只有一个 deadline、一个 approvalId、一份卡。
@@ -812,17 +864,30 @@ export class ToolApprovalCoordinator {
812
864
  // 跳过),而车4 的回决端点按 askId 仍决得动它:人有可能照着一份**不是行上那份**的卡做授权。
813
865
  // 采信行上一个**无界/非有限**的 deadline 同样危险:它会把 pending 条目与准入名额钉在那里远超配置窗。
814
866
  // 裁:两样任一不合 ⇒ 不编造替身、不发帧,**fail-safe 走 park**(行本身交给对账收敛器处理)。
815
- const persistedEnvelope = ApprovalCardEnvelopeSchema.safeParse(existingOrCreated.cardJson);
816
- 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;
817
869
  if (!persistedEnvelope.success || !sanePersistedDeadline) {
818
- 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)");
819
875
  releaseAdmission();
820
876
  return "unavailable"; // park 路由;绝不用一个编造的身份把这张卡推上 wire
821
877
  }
822
- persistedExpiresAtMs = existingOrCreated.expiresAtMs;
878
+ persistedExpiresAtMs = existingOrCreated.row.expiresAtMs;
823
879
  wireApprovalId = persistedEnvelope.data.approvalId;
824
880
  frame.approvalId = wireApprovalId; // 旧帧的去重键必须与新帧同值(两帧同带 approvalId 是消费端的对账口)
825
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;
826
891
  }
827
892
  catch (err) {
828
893
  this.noteStoreError(err, "ensureAsk");
@@ -15,11 +15,12 @@
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>>;
@@ -30,6 +31,19 @@ type AskExcluded = "preview" | "principal" | "requiresRealApproval" | "riskAxes"
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
@@ -270,19 +270,54 @@ export declare function workspaceChangedEventData(ev: {
270
270
  * resume 账本)都是租户可读面,而 operator 投影是 #154 的独立大件——按 core 写死的失败方向,**没有投影
271
271
  * 就不得披露**,所以本批先把整段剥掉止泄(此前 sync 腿的裸 catch-all 会把它连同 `lockedConfig` /
272
272
  * `compliance` / `memoryAdmission` / `retention` 四个治理面在场位原样透传给任意订阅者)。
273
- * 哪天 operator 投影落地,它是**另一个**构造器(按调用方身份分叉),不是往这里加键。
273
+ * operator 投影是**另一个**构造器({@link wiringManifestOperatorEventData},live 腿按调用方身份挑),
274
+ * 不是往这里加键 —— 两条 durable 腿没有调用方身份可挑,所以它们恒用本构造器。
274
275
  * ⚠️ 剥段还必须连**对该段取的哈希**一起剥:`configFingerprint` 同样不投影,理由见下方它那一行的旁注
275
276
  * (无盐哈希 + 4 布尔 = 16 组合的谕示器,把「结构上藏起来」的段原样还原出来)。
276
277
  *
277
278
  * 其余段逐键挑(禁 `{...manifest}`:1.72 task_progress 教训——未来 core 加段必须由人显式处置,而不是
278
279
  * 顺着 spread 裸上线,尤其当新段可能又是一个 audience 受限面时)。所有值都是引擎派生的闭枚举/布尔/
279
280
  * 有限数/不透明摘要串,**非用户内容 ⇒ 不脱敏**;畸形值一律按缺席处理(不铸假值,与 compacted 同规)。
281
+ *
282
+ * 🔴 **未标签面的准入裁定逐面记录在此,缺省不是「放行」**(#154 裁定 C)。core 只给 `governance` 标了
283
+ * audience,其余段没有标签 —— 没标签**不等于**可以默认投给租户,每一段都要有一次人的裁定,写在这里:
284
+ * · `interaction.posture`(三值:interactive / headless / absent)—— **裁:租户可见**。它是部署形的
285
+ * 自述(这条腿有没有人能答问题),消费端据此决定渲不渲交互面;不是治理秘密,也不构成任何治理位的
286
+ * 侧信道(它不参与 governance 那 4 个布尔的还原)。
287
+ * 未来 core 加一段而它没有 audience 标签时,**先在这里补一条裁定再决定挑不挑键** —— 段级完备性钉
288
+ * (test/wiring-manifest-projection.test.ts)会在那一刻先把人拦下来。
280
289
  */
281
290
  export declare function wiringManifestEventData(ev: {
282
291
  manifest?: unknown;
283
292
  eventId?: unknown;
284
293
  parentToolCallId?: unknown;
285
294
  }): Record<string, unknown>;
295
+ /**
296
+ * `wiring_manifest` 的 **operator 投影**(#154 件一)—— {@link wiringManifestEventData} 的租户形加上
297
+ * `governance` 段与 core 的真 `configFingerprint`。
298
+ *
299
+ * 🔴 分叉发生在**读口**,不在账本:两条 durable 腿(ledger-sink / resume)恒存剥后形,因为账本写的那一刻
300
+ * 不知道未来的读者是谁(`GET /v1/runs/:id/events` 任何 run owner 都读得到),往账本存全量 = 把 operator-only
301
+ * 面写进租户可读介质;存两份则是双写漂移源。所以只有 live 腿在**知道调用方身份**的那一刻挑构造器。
302
+ * 代价是 operator 看不到历史腿的 governance 段 —— 治理四布尔是部署形事实(不随 leg 漂),静态自检面
303
+ * (`GET /v1/diagnostics/wiring`)随时给出当前真值。
304
+ *
305
+ * 准入判据是 `explicitOperatorOk`(**不是** `isOperator`):空 `OPERATOR_PRINCIPALS` 必须是「没有人是
306
+ * operator」,不能是「所有人都是」—— 后者会让一个没配 operator 名单的部署把治理面发给每一个订阅者。
307
+ *
308
+ * `configFingerprint` 只在本构造器里透传:它是 `sha256(canonicalize(manifest 去 leg/去两个 provenance))`
309
+ * 前 16 hex,**无盐**且输入含整个 governance 段,而 governance 只有 4 个布尔 ⇒ 16 种组合。租户手里已有其余
310
+ * 全部哈希输入,发给租户等于把剥掉的四位原样还原(谕示器);发给 operator 无损 —— 同一份产物里四布尔本就
311
+ * 在场。
312
+ *
313
+ * governance **逐键挑**(禁 `{...governance}`):core 往这个段里加第五键时必须由人显式处置,而不是顺着
314
+ * spread 裸上线 —— 这个段的每一键都是部署治理面,新键的默认待遇必须是「不发」。
315
+ */
316
+ export declare function wiringManifestOperatorEventData(ev: {
317
+ manifest?: unknown;
318
+ eventId?: unknown;
319
+ parentToolCallId?: unknown;
320
+ }): Record<string, unknown>;
286
321
  /**
287
322
  * `human_input`(core 5.14.0 design/171 新 `TaskEvent` 臂)—— 「谁把什么喂进了这条 run」的生命周期账本
288
323
  * 帧(objective / 实时 steer / nextTurn / parked-steer 续跑 / wake 五个载体共用一个投影点)。
@@ -349,13 +349,22 @@ export function workspaceChangedEventData(ev) {
349
349
  * resume 账本)都是租户可读面,而 operator 投影是 #154 的独立大件——按 core 写死的失败方向,**没有投影
350
350
  * 就不得披露**,所以本批先把整段剥掉止泄(此前 sync 腿的裸 catch-all 会把它连同 `lockedConfig` /
351
351
  * `compliance` / `memoryAdmission` / `retention` 四个治理面在场位原样透传给任意订阅者)。
352
- * 哪天 operator 投影落地,它是**另一个**构造器(按调用方身份分叉),不是往这里加键。
352
+ * operator 投影是**另一个**构造器({@link wiringManifestOperatorEventData},live 腿按调用方身份挑),
353
+ * 不是往这里加键 —— 两条 durable 腿没有调用方身份可挑,所以它们恒用本构造器。
353
354
  * ⚠️ 剥段还必须连**对该段取的哈希**一起剥:`configFingerprint` 同样不投影,理由见下方它那一行的旁注
354
355
  * (无盐哈希 + 4 布尔 = 16 组合的谕示器,把「结构上藏起来」的段原样还原出来)。
355
356
  *
356
357
  * 其余段逐键挑(禁 `{...manifest}`:1.72 task_progress 教训——未来 core 加段必须由人显式处置,而不是
357
358
  * 顺着 spread 裸上线,尤其当新段可能又是一个 audience 受限面时)。所有值都是引擎派生的闭枚举/布尔/
358
359
  * 有限数/不透明摘要串,**非用户内容 ⇒ 不脱敏**;畸形值一律按缺席处理(不铸假值,与 compacted 同规)。
360
+ *
361
+ * 🔴 **未标签面的准入裁定逐面记录在此,缺省不是「放行」**(#154 裁定 C)。core 只给 `governance` 标了
362
+ * audience,其余段没有标签 —— 没标签**不等于**可以默认投给租户,每一段都要有一次人的裁定,写在这里:
363
+ * · `interaction.posture`(三值:interactive / headless / absent)—— **裁:租户可见**。它是部署形的
364
+ * 自述(这条腿有没有人能答问题),消费端据此决定渲不渲交互面;不是治理秘密,也不构成任何治理位的
365
+ * 侧信道(它不参与 governance 那 4 个布尔的还原)。
366
+ * 未来 core 加一段而它没有 audience 标签时,**先在这里补一条裁定再决定挑不挑键** —— 段级完备性钉
367
+ * (test/wiring-manifest-projection.test.ts)会在那一刻先把人拦下来。
359
368
  */
360
369
  export function wiringManifestEventData(ev) {
361
370
  // 形参照 `compactionOutcomeEventData` / `contextUsageEventData` 先例:全可选 `unknown` + 内部逐层窄化。
@@ -424,10 +433,54 @@ export function wiringManifestEventData(ev) {
424
433
  // 「结构上藏起来、经哈希谕示器泄出去」仍然是泄出去,core 写死的失败方向(no projection ⇒ do not
425
434
  // disclose)对侧信道一样适用。它的广告用途(判两条腿是不是同一套装配)在本仓与 SDK **今天零消费端**,
426
435
  // 所以直接不发,而不是另铸一个「只对已投影字段取哈希」的服务端自造值(那是新 wire 语义,要跨仓对表)。
427
- // 回补点:#154 operator 投影按调用方身份分叉,那条腿才带 core 的真 fingerprint。
436
+ // core fingerprint 的是 {@link wiringManifestOperatorEventData}(explicitOperatorOk 为真的 live
437
+ // 连接才走那条),对那个受众而言四布尔本就在同一份产物里,哈希不再是侧信道。
428
438
  ...identityFields(ev),
429
439
  };
430
440
  }
441
+ /**
442
+ * `wiring_manifest` 的 **operator 投影**(#154 件一)—— {@link wiringManifestEventData} 的租户形加上
443
+ * `governance` 段与 core 的真 `configFingerprint`。
444
+ *
445
+ * 🔴 分叉发生在**读口**,不在账本:两条 durable 腿(ledger-sink / resume)恒存剥后形,因为账本写的那一刻
446
+ * 不知道未来的读者是谁(`GET /v1/runs/:id/events` 任何 run owner 都读得到),往账本存全量 = 把 operator-only
447
+ * 面写进租户可读介质;存两份则是双写漂移源。所以只有 live 腿在**知道调用方身份**的那一刻挑构造器。
448
+ * 代价是 operator 看不到历史腿的 governance 段 —— 治理四布尔是部署形事实(不随 leg 漂),静态自检面
449
+ * (`GET /v1/diagnostics/wiring`)随时给出当前真值。
450
+ *
451
+ * 准入判据是 `explicitOperatorOk`(**不是** `isOperator`):空 `OPERATOR_PRINCIPALS` 必须是「没有人是
452
+ * operator」,不能是「所有人都是」—— 后者会让一个没配 operator 名单的部署把治理面发给每一个订阅者。
453
+ *
454
+ * `configFingerprint` 只在本构造器里透传:它是 `sha256(canonicalize(manifest 去 leg/去两个 provenance))`
455
+ * 前 16 hex,**无盐**且输入含整个 governance 段,而 governance 只有 4 个布尔 ⇒ 16 种组合。租户手里已有其余
456
+ * 全部哈希输入,发给租户等于把剥掉的四位原样还原(谕示器);发给 operator 无损 —— 同一份产物里四布尔本就
457
+ * 在场。
458
+ *
459
+ * governance **逐键挑**(禁 `{...governance}`):core 往这个段里加第五键时必须由人显式处置,而不是顺着
460
+ * spread 裸上线 —— 这个段的每一键都是部署治理面,新键的默认待遇必须是「不发」。
461
+ */
462
+ export function wiringManifestOperatorEventData(ev) {
463
+ // 租户形整体复用(不是复制一份键表):operator 面 ⊇ 租户面 是结构性的,两个构造器不可能在共有段上漂。
464
+ const tenant = wiringManifestEventData(ev);
465
+ // 窄读器与租户构造器内的孪生同形而独立 —— 那只构造器被 16 组合谕示器钉钉着,本件不改它一个字节。
466
+ const str = (v) => (typeof v === "string" && v.length > 0 ? v : undefined);
467
+ const bool = (v) => (typeof v === "boolean" ? v : undefined);
468
+ const sec = (v) => (typeof v === "object" && v !== null && !Array.isArray(v) ? { ...v } : undefined);
469
+ const m = sec(ev.manifest) ?? {};
470
+ const g = sec(m.governance) ?? {};
471
+ const governance = {
472
+ ...(str(g.audience) !== undefined ? { audience: g.audience } : {}),
473
+ ...(bool(g.lockedConfig) !== undefined ? { lockedConfig: g.lockedConfig } : {}),
474
+ ...(bool(g.compliance) !== undefined ? { compliance: g.compliance } : {}),
475
+ ...(bool(g.memoryAdmission) !== undefined ? { memoryAdmission: g.memoryAdmission } : {}),
476
+ ...(bool(g.retention) !== undefined ? { retention: g.retention } : {}),
477
+ };
478
+ return {
479
+ ...tenant,
480
+ ...(Object.keys(governance).length > 0 ? { governance } : {}),
481
+ ...(str(m.configFingerprint) !== undefined ? { configFingerprint: m.configFingerprint } : {}),
482
+ };
483
+ }
431
484
  /**
432
485
  * `human_input`(core 5.14.0 design/171 新 `TaskEvent` 臂)—— 「谁把什么喂进了这条 run」的生命周期账本
433
486
  * 帧(objective / 实时 steer / nextTurn / parked-steer 续跑 / wake 五个载体共用一个投影点)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.3.0",
3
+ "version": "7.5.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.14.0",
57
+ "@sema-agent/core": "^5.17.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.6.0",
72
+ "@sema-agent/sdk": "^6.8.0",
73
73
  "@types/libsodium-wrappers": "^0.7.14",
74
74
  "@types/node": "22.10.2",
75
75
  "@types/pg": "^8.20.0",
@@ -1,14 +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
- /** 词法文件系统是**空的**:没有任何路径存在。见文件头「绝对路径」那条的 🔴 段——这一面是 core 5.14.0
10
- * 把 `exists` 报错改判 `unresolvedSymlink`(⇒ sensitive policy deny)之后,「不看盘」这条公理在新契约
11
- * 下的**唯一**诚实表达;报错会被读成「探测失败」,而本 env 从来不探测。 */
12
- exists(_path: string): Promise<Result<boolean, FileError>>;
13
- }
14
- //# sourceMappingURL=lexical-path-env.d.ts.map