@sema-agent/server 7.55.0 → 7.57.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 (129) hide show
  1. package/MIGRATION.md +3 -3
  2. package/README.md +14 -7
  3. package/README.zh-CN.md +10 -6
  4. package/USAGE.md +207 -59
  5. package/deploy/sema-up/chart/values.yaml +1 -1
  6. package/dist/approval-ask-audit-store.d.ts +100 -1
  7. package/dist/approval-ask-audit-store.js +103 -2
  8. package/dist/approval-card.d.ts +148 -13
  9. package/dist/approval-card.js +82 -13
  10. package/dist/approval-content-kind.d.ts +22 -0
  11. package/dist/approval-content-kind.js +5 -0
  12. package/dist/approval.d.ts +31 -0
  13. package/dist/approval.js +18 -0
  14. package/dist/auto-mode-face.d.ts +111 -0
  15. package/dist/auto-mode-face.js +99 -0
  16. package/dist/bench/s1/arms.js +5 -5
  17. package/dist/bench/s1/live-deps.js +11 -11
  18. package/dist/bench/s1/run-firm.js +3 -0
  19. package/dist/bench/s1/runner-ctx.d.ts +4 -0
  20. package/dist/bench/s1/runner-ctx.js +7 -0
  21. package/dist/boot/config-center.js +23 -1
  22. package/dist/boot/coordinators.js +13 -1
  23. package/dist/boot/leader.d.ts +21 -0
  24. package/dist/boot/leader.js +6 -0
  25. package/dist/boot/parked-revive-gate.d.ts +7 -2
  26. package/dist/boot/parked-revive-gate.js +12 -1
  27. package/dist/boot/resolve-spec.d.ts +3 -0
  28. package/dist/boot/resolve-spec.js +3 -1
  29. package/dist/boot/runner-deps.d.ts +12 -0
  30. package/dist/boot/runner-deps.js +28 -3
  31. package/dist/boot/runtime-caps.d.ts +19 -8
  32. package/dist/boot/runtime-caps.js +50 -20
  33. package/dist/boot/session-shell-gate-registry.d.ts +39 -0
  34. package/dist/boot/session-shell-gate-registry.js +21 -0
  35. package/dist/config-catalog.js +14 -5
  36. package/dist/config-center/types.d.ts +2 -1
  37. package/dist/config-provider.js +1 -0
  38. package/dist/config-types.d.ts +41 -10
  39. package/dist/config.d.ts +21 -0
  40. package/dist/config.js +40 -5
  41. package/dist/http/route-ctx.d.ts +90 -3
  42. package/dist/http/routes/a2a-serve.js +5 -3
  43. package/dist/http/routes/approvals-assistant.js +21 -8
  44. package/dist/http/routes/capabilities.js +21 -4
  45. package/dist/http/routes/leader.js +2 -2
  46. package/dist/http/routes/memory-origin.d.ts +2 -2
  47. package/dist/http/routes/rules.js +3 -2
  48. package/dist/http/routes/runs.d.ts +0 -14
  49. package/dist/http/routes/runs.js +20 -8
  50. package/dist/http/routes/tasks.js +34 -8
  51. package/dist/http/routes/workflows.js +20 -5
  52. package/dist/http/server.d.ts +11 -1
  53. package/dist/http/server.js +220 -34
  54. package/dist/http/wire-types.d.ts +9 -4
  55. package/dist/leader/diffout.js +2 -1
  56. package/dist/leader/endpoint.js +49 -15
  57. package/dist/leader/fanout.js +6 -5
  58. package/dist/leader/leader.js +17 -14
  59. package/dist/leader/merge.js +17 -12
  60. package/dist/leader/planner.js +3 -2
  61. package/dist/leader/repair-oracle.js +6 -4
  62. package/dist/leader/repair-wire.js +5 -3
  63. package/dist/leader/wire.d.ts +80 -1
  64. package/dist/leader/wire.js +55 -26
  65. package/dist/main.js +44 -8
  66. package/dist/observability/err-text.d.ts +6 -0
  67. package/dist/observability/err-text.js +10 -0
  68. package/dist/observability/fail-open.d.ts +40 -0
  69. package/dist/observability/fail-open.js +19 -0
  70. package/dist/observability/metrics.js +1 -1
  71. package/dist/observability/run-terminal-log.d.ts +120 -0
  72. package/dist/observability/run-terminal-log.js +360 -0
  73. package/dist/orchestration/workflow-completion-inbox.d.ts +9 -0
  74. package/dist/orchestration/workflow-completion-inbox.js +8 -0
  75. package/dist/permission-rule-vocab.d.ts +40 -0
  76. package/dist/permission-rule-vocab.js +17 -0
  77. package/dist/plugins/checkpoint-store-sql.d.ts +26 -0
  78. package/dist/plugins/checkpoint-store-sql.js +23 -4
  79. package/dist/plugins/file-run-store.d.ts +3 -34
  80. package/dist/plugins/file-run-store.js +21 -0
  81. package/dist/plugins/local-checkpoint-store.d.ts +10 -0
  82. package/dist/plugins/local-checkpoint-store.js +5 -0
  83. package/dist/plugins/local-session-store.d.ts +9 -9
  84. package/dist/plugins/local-session-store.js +5 -3
  85. package/dist/plugins/memory-run-store.d.ts +3 -15
  86. package/dist/plugins/memory-run-store.js +21 -0
  87. package/dist/plugins/permission-rule-store-file.d.ts +22 -4
  88. package/dist/plugins/permission-rule-store-file.js +25 -11
  89. package/dist/plugins/permission-rule-store-sql.d.ts +60 -30
  90. package/dist/plugins/permission-rule-store-sql.js +39 -23
  91. package/dist/plugins/pg-session-storage.js +17 -13
  92. package/dist/plugins/remote-scratchpad.js +3 -2
  93. package/dist/plugins/run-store-sql.d.ts +3 -42
  94. package/dist/plugins/run-store-sql.js +36 -5
  95. package/dist/plugins/store-backend.d.ts +1 -1
  96. package/dist/plugins/store-contracts.d.ts +70 -1
  97. package/dist/plugins/tidb-session-store.js +18 -14
  98. package/dist/plugins/workflow-run-store-sql.d.ts +5 -0
  99. package/dist/plugins/workflow-run-store-sql.js +10 -2
  100. package/dist/rules-consent.d.ts +5 -4
  101. package/dist/rules-consent.js +25 -46
  102. package/dist/run-cancel-context.d.ts +13 -0
  103. package/dist/run-cancel-context.js +15 -0
  104. package/dist/run-local.js +2 -2
  105. package/dist/runs.d.ts +4 -1
  106. package/dist/runs.js +41 -10
  107. package/dist/runtime-caps-resolver.d.ts +133 -17
  108. package/dist/runtime-caps-resolver.js +44 -4
  109. package/dist/security.d.ts +7 -0
  110. package/dist/task-settings.d.ts +57 -3
  111. package/dist/task-settings.js +78 -5
  112. package/dist/task-workflow.d.ts +32 -2
  113. package/dist/task-workflow.js +8 -3
  114. package/dist/tool-approval.d.ts +38 -63
  115. package/dist/tool-approval.js +102 -74
  116. package/dist/trace/core-keyset-guard.d.ts +2 -2
  117. package/dist/trace/ledger-sink.d.ts +13 -4
  118. package/dist/trace/ledger-sink.js +14 -6
  119. package/dist/trace/project.d.ts +13 -0
  120. package/dist/trace/project.js +10 -1
  121. package/dist/trace/redact.d.ts +22 -11
  122. package/dist/trace/redact.js +655 -20
  123. package/dist/trace/sema-provenance.d.ts +26 -0
  124. package/dist/trace/sema-provenance.js +11 -0
  125. package/dist/turn-activity.d.ts +32 -2
  126. package/dist/turn-activity.js +29 -4
  127. package/package.json +3 -3
  128. package/skills/find-skills.md +1 -1
  129. package/skills/loop.md +1 -1
@@ -77,7 +77,7 @@ registry:
77
77
  nodePort: 30300
78
78
  storageSize: 2Gi
79
79
  storageClass: "" # 空=集群默认 SC
80
- # [4824] S10 P1:默认管理员名必须是中性值——个人标识串作生产默认值=内部标识外溢(cli 点名案)。
80
+ # 默认管理员名必须是中性值——个人标识串作生产默认值=内部标识外溢(cli 点名案)。
81
81
  adminUser: admin
82
82
  adminPassword: "" # REQUIRED(kube-up 注入)
83
83
  sessionSecret: "" # REQUIRED
@@ -28,13 +28,49 @@ export interface AuditSettleInput {
28
28
  settledBy?: string;
29
29
  settledAtMs: number;
30
30
  }
31
+ /**
32
+ * [ref] **迟到人工结算点**入参:调用方(两条 HTTP decide 腿)只交 **checkpoint 锚 + 这次结算的事实**,
33
+ * `approvalId` 的反查与「算不算迟到」的判据**属主在店内**(一处成文,见 {@link FileApprovalAskAuditStore.recordLateSettle})
34
+ * —— 路由层没有账本索引,让它自己推会造出第二份判据。
35
+ */
36
+ export interface AuditLateSettleInput {
37
+ /** checkpoint 锚①:被决议那条 durable 行的 sessionId(`/v1/approvals/:sessionId/decide` 的那把;
38
+ * parked 子代形下 = **子会话 id**,与 ask 行的 `sourceTaskId` 同值)。 */
39
+ sessionId: string;
40
+ /** checkpoint 锚②:`pendingAction.toolCallId`(tool_approval 门恒在场;别的审批门型可缺席)。 */
41
+ toolCallId?: string;
42
+ /** checkpoint 锚③:resume 凭据的**哈希**({@link buildCheckpointAnchorHash};token 本身是 token-as-auth
43
+ * 的能力凭据,**绝不**落审计盘 —— 账本 0o600 也不是把凭据写进去的理由)。 */
44
+ checkpointTokenHash?: string;
45
+ /** 这次人工结算的结果(approve/deny 逐字映射;`expired` 不是本记录的合法值 —— 它是流窗的词)。 */
46
+ outcome: "allowed" | "denied";
47
+ /** core `ApprovalSettledBy` 逐字(调用方给,店**不派生**:集中推导正是「窗到期被报成另一个人拒绝」
48
+ * 的成因,[ref] 件6③ 同律)。今天两条腿恒 `"human"`,但真值以调用方为准。 */
49
+ settledBy: string;
50
+ /** 记录时刻 = 那条腿**权威受理**的时刻(不是请求到达的时刻)—— 落行点在 200 之后,见记录种类表。 */
51
+ settledAtMs: number;
52
+ }
31
53
  /** 协调器消费的窄写口(协调器只 import type,不拖实现依赖)。 */
32
54
  export interface ApprovalAskAuditSink {
33
55
  recordAsk(input: AuditAskInput): void;
34
56
  recordSettle(input: AuditSettleInput): void;
35
57
  /** 腿闭点(`runWithContext` finally)。只为铸过审计行的 taskId 真落行(店内去重)。 */
36
58
  noteLegClosed(taskId: string): void;
59
+ /** [ref] 迟到人工结算点(HTTP decide 两腿)。**纯观察**:自吞一切失败,不返回受否,绝不改变结算面。 */
60
+ recordLateSettle(input: AuditLateSettleInput): void;
37
61
  }
62
+ /**
63
+ * [ref]:decide 两腿消费的**更窄写口** —— 只有迟到结算这一个动词。
64
+ * 刻意不把整个 {@link ApprovalAskAuditSink} 递给 HTTP 层:铸造 / 决议 / 腿闭三点的属主是协调器,
65
+ * 路由层拿到它们只会长出第二个写者(与 {@link ApprovalAskAuditReadFace} 同一条窄口纪律)。
66
+ */
67
+ export type ApprovalLateSettleSink = Pick<ApprovalAskAuditSink, "recordLateSettle">;
68
+ /**
69
+ * checkpoint resume 凭据 → 审计行可落的锚(sha256 前 16 hex)。
70
+ * 单向、不可逆、不足以拿去 resume;够用来把审计行 join 回 checkpoint 平面(取证时对手上的 token 现算)。
71
+ * 形与长度的唯一属主在此(两条腿都调它,不各自 `createHash`)。
72
+ */
73
+ export declare function buildCheckpointAnchorHash(token: string): string;
38
74
  /** v1.1 §6 两分臂(闭集)。 */
39
75
  export type CrashOrphanState = "pending" | "decided";
40
76
  /** `GET /v1/approvals` additive 键 `crashConverged` 的行形(wire 键集刻意窄:args/卡体不上列表,
@@ -63,6 +99,15 @@ export interface CrashConvergedRow {
63
99
  export interface ApprovalAskAuditReadFace {
64
100
  listCrashConverged(scope: string | undefined): CrashConvergedRow[];
65
101
  }
102
+ /**
103
+ * [ref]:`late_settle` 行上 `approvalId === null` 的**成因词**(闭集;缺席 = 反查唯一命中,身份是真的)。
104
+ * 「反查不到不得静默」:身份说不清的时候写的是「说不清 + 为什么」,不是不写、也不是编一个 id。
105
+ * 闭集纪律([ref] + 本店 `crash_converged.orphanState` 先例):重放读到**词表外**的值 ⇒ 整行折坏行计数,
106
+ * 不折 `undefined` —— 折了就等于把「说不清且原因是 X」静默降级成「说不清、没原因」。
107
+ * 与身份字段的**交叉不变式**(写口自带,重放侧同样强制):`approvalId === null` ⇔ 本词在场;身份说不清
108
+ * 时 `taskId` 必须缺席(挑一条铸造行填 = 张冠李戴)。
109
+ */
110
+ export type LateSettleUnresolved = "ambiguous_anchor";
66
111
  export declare class FileApprovalAskAuditStore implements ApprovalAskAuditSink, ApprovalAskAuditReadFace {
67
112
  /** 账本绝对路径(测试/取证按文件字节对账 —— 崩溃语义的真源是文件,不是内存投影)。 */
68
113
  readonly ledgerPath: string;
@@ -83,6 +128,21 @@ export declare class FileApprovalAskAuditStore implements ApprovalAskAuditSink,
83
128
  /** 本进程铸过审计行、且腿闭信号可达的 taskId(腿闭去重 + 防无关腿刷账本)。 */
84
129
  private readonly openLegTasks;
85
130
  private convergedOnce;
131
+ /**
132
+ * [ref] 反查索引 ①:**checkpoint 锚 → 铸造行身份**。与 {@link replayed} 刻意分家 —— 那张表是
133
+ * 「前世账本」快照(收敛只对前世行),而迟到决议要认的是**前世 + 本进程**两段账(人可以在重启前批,
134
+ * 也可以在重启后批)。键形 = {@link FileApprovalAskAuditStore.callAnchorKey}(会话轴的两把键见
135
+ * {@link FileApprovalAskAuditStore.indexAsk});弱桶另见下一个字段。
136
+ */
137
+ private readonly askByCallAnchor;
138
+ /** [ref] 反查索引 ①b:**无 toolCallId** 的铸造行(只能按会话认);与 A 桶分开是为了不让「有 callId 的
139
+ * 锚」误命中「另一只没写 callId 的 ask」—— 归并会把两条不同的 ask 折成一条。 */
140
+ private readonly askBySessionOnly;
141
+ /** [ref] 反查索引 ②:走过 park 路由的窗到期(`settle{outcome:"expired", parkRouted:true}`)——
142
+ * 「这次决议算不算迟到」的**唯一**判据来源。 */
143
+ private readonly parkExpired;
144
+ /** [ref] 反查索引 ③:approvalId → 铸造坐标(命中后回填 late 行的 `taskId`)。 */
145
+ private readonly askTaskId;
86
146
  /**
87
147
  * boot 期 fail-stop:mkdir/重放失败**拒启**(File 店族同律)。root = local 数据根
88
148
  * (`config.localDataRoot`;与 `LocalBackend` 同源,见头注装配段)。
@@ -103,11 +163,50 @@ export declare class FileApprovalAskAuditStore implements ApprovalAskAuditSink,
103
163
  private group;
104
164
  /** append + fsync(逐行落盘:进程猝死不丢已记的行 —— 本店的全部价值就在崩溃时刻)。
105
165
  * 运行期失败**响亮不阻断**:审批可用性不押在审计盘上(盘满/只读时人还得能批),但丢行必须在遥测
106
- * 显形([ref] 三件套,census 登记行同批)。 */
166
+ * 显形([ref] 三件套,census 登记行同批)。
167
+ * 🔴 返回**这条记录是否真的落盘并 fsync 成功**([ref] codex R1-F2 采):迟到结算的 late 判据只认
168
+ * 落了盘的前置事实 —— 调用方据此决定要不要把这条记录喂进反查索引(见 {@link recordAsk}/
169
+ * {@link recordSettle})。没有这个返回值,索引会把一条**只在内存里**的 ask/park 事实当证据,
170
+ * 后来铸出的 `late_settle` 行在同一个账本文件里无法复核。 */
107
171
  private append;
172
+ /** [ref] 锚键 A(精确):会话 + 工具调用 id。会话轴登记**两把** —— 见 {@link indexAsk}。 */
173
+ private static callAnchorKey;
174
+ /**
175
+ * [ref]:把一条铸造行放进反查索引。
176
+ * 会话轴登记 `sessionId` 与 `sourceTaskId` **两把**:parked 后台子代的 checkpoint 上
177
+ * `cp.sessionId` 是**子会话 id**(= ask 行的 `sourceTaskId`),而 ask 行的 `sessionId` 是**宿主**
178
+ * 会话(铸造点取的是 `origin.sessionId`,继承链闭包携宿主身份)—— 只登记一把会让委派子代那条腿
179
+ * 的迟到决议恒反查不到。顶层 run 上两者同值,登记去重后是同一把。
180
+ */
181
+ private indexAsk;
182
+ /** [ref]:决议行进 late 判据索引(只认 `expired ∧ parkRouted` —— 别的终局都不是「转 durable 等人」)。 */
183
+ private indexSettle;
108
184
  recordAsk(input: AuditAskInput): void;
109
185
  recordSettle(input: AuditSettleInput): void;
110
186
  noteLegClosed(taskId: string): void;
187
+ /**
188
+ * [ref] **迟到人工结算点**(判据的唯一属主;两条 HTTP decide 腿只交锚,不自己推)。
189
+ *
190
+ * 三步:
191
+ * 1. **反查**:checkpoint 锚 → 铸造行身份。**锚带 `toolCallId` 时,`(会话, toolCallId)` 精确桶是
192
+ * 唯一权威**;两侧都没有调用锚时才退到会话弱桶(= 该会话下没写 toolCallId 的铸造行)。会话轴
193
+ * 登记两把(宿主会话 / 子会话,见 {@link indexAsk})。
194
+ * 🔴 **弱桶绝不与精确桶合并**(codex R1-F3 采,两条真路径):①同会话有一条**无调用锚**的历史
195
+ * park 行、而本次决的那只 checkpoint 从未在本账本留过铸造行(headless park / 换过账本,头注明写
196
+ * 这是合法形)⇒ 合并后唯一候选是那条历史行,行会写成「approvalId/taskId = 历史那只、toolCallId =
197
+ * 这一只」的自相矛盾审计行;②精确唯一命中被同会话弱候选拖成假 `ambiguous_anchor`,一个**本可
198
+ * 确定**的身份被降级。弱证据不足以证明「这一次决的就是它」,证不出就不写(步骤 2 同一条纪律)。
199
+ * 2. **late 门**:候选里筛出账本已有 `settle{outcome:"expired", parkRouted:true}` 的那些。
200
+ * 筛空 ⇒ **一行不写**。理由是不编事实:证不出「窗到期已转 durable」,这次决议就只是一次普通的
201
+ * durable decide(headless 提交的 park、SQL 车道搬来的账本、或本店根本没记过这只 ask),
202
+ * 写 `lateAfterExpiry:true` 会把推断当成事实上账 —— 那比缺一行更坏(审计行是要被当证据引的)。
203
+ * 3. **落行**:唯一命中 ⇒ 带真 `approvalId` + 铸造 `taskId`;多条同锚命中 ⇒ `approvalId:null` +
204
+ * `unresolved:"ambiguous_anchor"`(**不静默**:身份说不清照样留行,锚全在行上给人去 join)。
205
+ *
206
+ * 纯观察:自吞一切失败(append 的响亮留痕在 {@link append} 里),不返回受否 —— 结算面绝不因审计写不动
207
+ * 而改变(与 `recordAsk`/`recordSettle` 同姿态,裁 (c) 的行为面红线)。
208
+ */
209
+ recordLateSettle(input: AuditLateSettleInput): void;
111
210
  /**
112
211
  * boot 崩溃收敛器(装配层在构造后、协调器开始服务前调一次;幂等跨 boot)。
113
212
  * 判据 = 头注「boot 收敛判据」段(一处成文,不复述)。返回两分臂计数 + 坏行数供 boot 日志。
@@ -1,8 +1,12 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, writeSync } from "node:fs";
2
3
  import { join } from "node:path";
3
4
  import { recordFailOpen } from "./observability/fail-open.js";
4
5
  export const APPROVAL_ASK_AUDIT_DIR = "approval-ask-audit";
5
6
  export const CRASH_CONVERGED_LIST_MAX = 200;
7
+ export function buildCheckpointAnchorHash(token) {
8
+ return createHash("sha256").update(token, "utf8").digest("hex").slice(0, 16);
9
+ }
6
10
  function readLedgerRecord(u) {
7
11
  if (u === null || typeof u !== "object" || Array.isArray(u))
8
12
  return undefined;
@@ -75,6 +79,43 @@ function readLedgerRecord(u) {
75
79
  return undefined;
76
80
  return { kind: "crash_converged", approvalId, orphanState, convergedAtMs };
77
81
  }
82
+ case "late_settle": {
83
+ const idV = o.approvalId;
84
+ const approvalId = typeof idV === "string" ? idV : idV === null ? null : undefined;
85
+ const outcomeV = o.outcome;
86
+ const outcome = outcomeV === "allowed" || outcomeV === "denied" ? outcomeV : undefined;
87
+ const settledBy = s("settledBy");
88
+ const settledAtMs = num("settledAtMs");
89
+ const sessionId = s("sessionId");
90
+ if (approvalId === undefined || outcome === undefined || settledBy === undefined || settledAtMs === undefined || sessionId === undefined)
91
+ return undefined;
92
+ if (bool("lateAfterExpiry") !== true)
93
+ return undefined;
94
+ const toolCallId = s("toolCallId");
95
+ const checkpointTokenHash = s("checkpointTokenHash");
96
+ const taskId = s("taskId");
97
+ const unresolvedV = o.unresolved;
98
+ if (unresolvedV !== undefined && unresolvedV !== "ambiguous_anchor")
99
+ return undefined;
100
+ const unresolved = unresolvedV === "ambiguous_anchor" ? "ambiguous_anchor" : undefined;
101
+ if ((approvalId === null) !== (unresolved !== undefined))
102
+ return undefined;
103
+ if (approvalId === null && taskId !== undefined)
104
+ return undefined;
105
+ return {
106
+ kind: "late_settle",
107
+ approvalId,
108
+ outcome,
109
+ settledBy,
110
+ lateAfterExpiry: true,
111
+ settledAtMs,
112
+ sessionId,
113
+ ...(toolCallId !== undefined ? { toolCallId } : {}),
114
+ ...(checkpointTokenHash !== undefined ? { checkpointTokenHash } : {}),
115
+ ...(taskId !== undefined ? { taskId } : {}),
116
+ ...(unresolved !== undefined ? { unresolved } : {}),
117
+ };
118
+ }
78
119
  default:
79
120
  return undefined;
80
121
  }
@@ -89,6 +130,10 @@ export class FileApprovalAskAuditStore {
89
130
  convergedRows = [];
90
131
  openLegTasks = new Set();
91
132
  convergedOnce = false;
133
+ askByCallAnchor = new Map();
134
+ askBySessionOnly = new Map();
135
+ parkExpired = new Set();
136
+ askTaskId = new Map();
92
137
  constructor(root, internals) {
93
138
  this.writeBytes = internals?.writeBytes ?? ((fd, buf, off, len) => writeSync(fd, buf, off, len));
94
139
  const dir = join(root, APPROVAL_ASK_AUDIT_DIR);
@@ -133,6 +178,7 @@ export class FileApprovalAskAuditStore {
133
178
  g.ask = rec;
134
179
  g.askLine = line;
135
180
  }
181
+ this.indexAsk(rec);
136
182
  break;
137
183
  }
138
184
  case "settle": {
@@ -141,8 +187,11 @@ export class FileApprovalAskAuditStore {
141
187
  g.settle = rec;
142
188
  g.settleLine = line;
143
189
  }
190
+ this.indexSettle(rec);
144
191
  break;
145
192
  }
193
+ case "late_settle":
194
+ break;
146
195
  case "leg_closed":
147
196
  this.replayedLegClosed.push({ taskId: rec.taskId, line });
148
197
  break;
@@ -183,19 +232,48 @@ export class FileApprovalAskAuditStore {
183
232
  while (off < buf.length)
184
233
  off += this.writeBytes(this.fd, buf, off, buf.length - off);
185
234
  fsyncSync(this.fd);
235
+ return true;
186
236
  }
187
237
  catch (err) {
188
238
  this.tailDirty = true;
189
239
  recordFailOpen("server.approval-ask-audit.append-failed", `kind=${rec.kind} err=${err instanceof Error ? err.message : String(err)}`);
240
+ return false;
241
+ }
242
+ }
243
+ static callAnchorKey(session, toolCallId) {
244
+ return `${session}\u0000${toolCallId}`;
245
+ }
246
+ indexAsk(rec) {
247
+ this.askTaskId.set(rec.approvalId, rec.taskId);
248
+ const sessions = new Set();
249
+ if (rec.sessionId !== undefined)
250
+ sessions.add(rec.sessionId);
251
+ if (rec.sourceTaskId !== undefined)
252
+ sessions.add(rec.sourceTaskId);
253
+ for (const session of sessions) {
254
+ const bucket = rec.toolCallId !== undefined ? this.askByCallAnchor : this.askBySessionOnly;
255
+ const key = rec.toolCallId !== undefined ? FileApprovalAskAuditStore.callAnchorKey(session, rec.toolCallId) : session;
256
+ let ids = bucket.get(key);
257
+ if (!ids) {
258
+ ids = new Set();
259
+ bucket.set(key, ids);
260
+ }
261
+ ids.add(rec.approvalId);
190
262
  }
191
263
  }
264
+ indexSettle(rec) {
265
+ if (rec.outcome === "expired" && rec.parkRouted === true)
266
+ this.parkExpired.add(rec.approvalId);
267
+ }
192
268
  recordAsk(input) {
193
269
  if (input.legTracked)
194
270
  this.openLegTasks.add(input.taskId);
195
- this.append({ kind: "ask", ...input });
271
+ if (this.append({ kind: "ask", ...input }))
272
+ this.indexAsk(input);
196
273
  }
197
274
  recordSettle(input) {
198
- this.append({ kind: "settle", ...input });
275
+ if (this.append({ kind: "settle", ...input }))
276
+ this.indexSettle(input);
199
277
  }
200
278
  noteLegClosed(taskId) {
201
279
  if (!this.openLegTasks.has(taskId))
@@ -203,6 +281,29 @@ export class FileApprovalAskAuditStore {
203
281
  this.openLegTasks.delete(taskId);
204
282
  this.append({ kind: "leg_closed", taskId, closedAtMs: Date.now() });
205
283
  }
284
+ recordLateSettle(input) {
285
+ const candidates = input.toolCallId !== undefined
286
+ ? (this.askByCallAnchor.get(FileApprovalAskAuditStore.callAnchorKey(input.sessionId, input.toolCallId)) ?? new Set())
287
+ : (this.askBySessionOnly.get(input.sessionId) ?? new Set());
288
+ const parked = [...candidates].filter((id) => this.parkExpired.has(id));
289
+ if (parked.length === 0)
290
+ return;
291
+ const only = parked.length === 1 ? parked[0] : undefined;
292
+ const taskId = only !== undefined ? this.askTaskId.get(only) : undefined;
293
+ this.append({
294
+ kind: "late_settle",
295
+ approvalId: only ?? null,
296
+ outcome: input.outcome,
297
+ settledBy: input.settledBy,
298
+ lateAfterExpiry: true,
299
+ settledAtMs: input.settledAtMs,
300
+ sessionId: input.sessionId,
301
+ ...(input.toolCallId !== undefined ? { toolCallId: input.toolCallId } : {}),
302
+ ...(input.checkpointTokenHash !== undefined ? { checkpointTokenHash: input.checkpointTokenHash } : {}),
303
+ ...(taskId !== undefined ? { taskId } : {}),
304
+ ...(only === undefined ? { unresolved: "ambiguous_anchor" } : {}),
305
+ });
306
+ }
206
307
  convergeCrashedAsks() {
207
308
  if (this.convergedOnce)
208
309
  return { pending: 0, decided: 0, inconsistent: 0, corruptLines: this.corruptLines };
@@ -49,30 +49,76 @@ export declare const MAX_RULE_OFFERS = 4;
49
49
  * 几条规则),混成一个数会让「上游把 batch 放宽到 6 条成员」误伤到 offer 条数这条无关轴。
50
50
  */
51
51
  export declare const MAX_RULE_OFFER_BATCH_MEMBERS = 8;
52
+ /**
53
+ * **一条**「不再询问」OFFER 的形(core 5.58.0 / [ref] §3.1 —— **BREAKING**,取代退役的
54
+ * `RuleSuggestion`)。同步腿(`ApprovalCardSchema.ruleOffers` / `card_json`)与耐久腿(durable park 行的
55
+ * `PendingCheckpoint.ruleOffers`,由 `plugins/checkpoint-store-sql.ts` 窄读)**共用这一份**。
56
+ *
57
+ * 🔴 单一属主(宪法 [ref]「schema 单一属主禁复制」):两条腿投的是 core 的**同一个** `RuleOffer`
58
+ * (同步腿 = `AskRequest.ruleOffers`,耐久腿 = `PendingAction.ruleOffers`,core
59
+ * `checkpoint-store.d.ts` 明写「CONTRACT (same as the synchronous `AskRequest.ruleOffers`)」)。
60
+ * 各写一份 zod 必然漂——闭词表 `match`/`kind` 加词、上限改口径,只改一处就出两种形。
61
+ *
62
+ * 🔴 **闭集判别联合,`kind` 是判别位**(core d.ts 逐字:"A CLOSED discriminated union"):
63
+ * · `single` —— 一条覆盖**整串**命令的规则(exact 精确形,或简单命令的 reviewed prefix 形);
64
+ * · `batch` —— 复合命令的**逐段合取**批:选它 = **一次对全部 `rules` 说 yes**,批内**无**逐条子选择。
65
+ * 成员是判别联合([ref] B3:`command` 逐段 Bash 规则 | `directoryRead` cd 段铸的目录只读授权)。
66
+ * `uncoveredSegments` 是铸批时刻的**诚实溢出披露**(既不被本批覆盖、也不被卡的覆盖快照覆盖的段数),
67
+ * `0` 读作「这批兑完,这条复合命令在那份快照的眼里就全覆盖了」;`uncoveredDetail`(§3.5,additive)
68
+ * 逐段说**为什么**还没被覆盖(闭三词集),行数恒等于 count(count 是真源,明细缺席不是断言)。
69
+ *
70
+ * 🔴 **序即契约**(core 逐字,消费端可以依赖):至多 2 条;whole-string exact single 在场时**恒 index 0**;
71
+ * batch 至多一条、**恒末位**。所以本仓一切截断都必须是**取前缀**(保序、保原始下标),绝不重排。
72
+ */
73
+ /**
74
+ * [ref] §3.5 —— `uncoveredDetail` **行数**的 server 容忍帽(理由与 {@link MAX_RULE_OFFER_BATCH_MEMBERS}
75
+ * 同源:纯展示轴上的一条改坏/放宽的上游不该把一行撑爆)。⚠️ 超帽的处置是**丢整座**而不是截断:core 契约
76
+ * 明写「`uncoveredDetail.length === uncoveredSegments` whenever the seat is present」且「count 座是真源、
77
+ * 明细缺席不是断言」——截过的座会让一个按契约校验等式的消费端把整只 batch 判假,缺席则只是少一行解释。
78
+ */
79
+ export declare const MAX_UNCOVERED_DETAIL_ROWS = 32;
52
80
  export declare const RuleOfferSchema: z.ZodUnion<readonly [z.ZodObject<{
53
81
  kind: z.ZodLiteral<"single">;
54
82
  rule: z.ZodString;
55
83
  match: z.ZodEnum<{
56
84
  exact: "exact";
57
85
  prefix: "prefix";
86
+ wildcard: "wildcard";
87
+ subpath: "subpath";
58
88
  }>;
59
89
  command: z.ZodString;
60
90
  }, z.core.$strict>, z.ZodObject<{
61
91
  kind: z.ZodLiteral<"batch">;
62
- rules: z.ZodArray<z.ZodObject<{
92
+ rules: z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
93
+ kind: z.ZodLiteral<"command">;
63
94
  rule: z.ZodString;
64
95
  match: z.ZodEnum<{
65
96
  exact: "exact";
66
97
  prefix: "prefix";
98
+ wildcard: "wildcard";
99
+ subpath: "subpath";
67
100
  }>;
68
101
  command: z.ZodString;
69
102
  segment: z.ZodString;
70
- }, z.core.$strict>>;
103
+ }, z.core.$strict>, z.ZodObject<{
104
+ kind: z.ZodLiteral<"directoryRead">;
105
+ rule: z.ZodString;
106
+ directory: z.ZodString;
107
+ segment: z.ZodString;
108
+ }, z.core.$strict>]>>;
71
109
  uncoveredSegments: z.ZodNumber;
110
+ uncoveredDetail: z.ZodOptional<z.ZodArray<z.ZodObject<{
111
+ segment: z.ZodString;
112
+ reason: z.ZodEnum<{
113
+ redirection: "redirection";
114
+ no_rule_form: "no_rule_form";
115
+ cap_overflow: "cap_overflow";
116
+ }>;
117
+ }, z.core.$strict>>>;
72
118
  }, z.core.$strict>]>;
73
119
  /**
74
- * {@link RuleOfferSchema} 的**不限长**孪生 —— 只判**结构**(闭词表 `kind`/`match` + 若干字符串),长度由
75
- * 调用方在**脱敏之后**自己截。
120
+ * {@link RuleOfferSchema} 的**不限长**孪生 —— 只判**结构**(闭词表 `kind`/`match`/`reason` + 若干字符串),
121
+ * 长度由调用方在**脱敏之后**自己截。
76
122
  *
77
123
  * 🔴 为什么必须有它(codex 交叉复审 [medium],验真后修):`redactSecrets` **会变长**(一条
78
124
  * `password=…` 换成更长的遮蔽标记)。拿带 `.max()` 的形去校验**脱敏前**的原文,再脱敏,产出的就可能是
@@ -100,7 +146,11 @@ export declare const RuleOfferSchema: z.ZodUnion<readonly [z.ZodObject<{
100
146
  * 落库/读回的 {@link ApprovalCardSchema} 仍 `.strict()`(投影只写已知键,多余键根本到不了那一层)。
101
147
  *
102
148
  * 🔴 **判别联合上的 strip 要逐臂加**(zod 4:`z.union` 自己没有 unknownKeys 政策,政策在成员上)——
103
- * 两臂各自 `.strip()`,否则 batch 臂对 additive 加键仍是 strict 的,而那正是本注要拆的雷。
149
+ * 两臂各自 `.strip()`,batch 成员的两臂亦然,否则对 additive 加键仍是 strict 的,而那正是本注要拆的雷。
150
+ *
151
+ * 🔴 **闭词表不在放宽之列**([ref] B3 与 [ref] 词表纪律):`kind`(offer 与成员两级)/`match`/`reason`
152
+ * 是闭集 —— 集外词 = 判假 = 整只 offer 丢(成员级的未知 `kind` 丢的是整只 batch,不是那一个成员;理由见
153
+ * {@link RuleOfferBatchMemberSchema} 顶注)。「additive 加键照收」与「闭集加词即拒」是两条轴,别混。
104
154
  */
105
155
  export declare const RuleOfferRawSchema: z.ZodUnion<readonly [z.ZodObject<{
106
156
  kind: z.ZodLiteral<"single">;
@@ -108,25 +158,69 @@ export declare const RuleOfferRawSchema: z.ZodUnion<readonly [z.ZodObject<{
108
158
  match: z.ZodEnum<{
109
159
  exact: "exact";
110
160
  prefix: "prefix";
161
+ wildcard: "wildcard";
162
+ subpath: "subpath";
111
163
  }>;
112
164
  command: z.ZodString;
113
165
  }, z.core.$strip>, z.ZodObject<{
114
166
  kind: z.ZodLiteral<"batch">;
115
- rules: z.ZodArray<z.ZodObject<{
167
+ rules: z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
168
+ kind: z.ZodLiteral<"command">;
116
169
  rule: z.ZodString;
117
170
  match: z.ZodEnum<{
118
171
  exact: "exact";
119
172
  prefix: "prefix";
173
+ wildcard: "wildcard";
174
+ subpath: "subpath";
120
175
  }>;
121
176
  command: z.ZodString;
122
177
  segment: z.ZodString;
123
- }, z.core.$strip>>;
178
+ }, z.core.$strip>, z.ZodObject<{
179
+ kind: z.ZodLiteral<"directoryRead">;
180
+ rule: z.ZodString;
181
+ directory: z.ZodString;
182
+ segment: z.ZodString;
183
+ }, z.core.$strip>]>>;
124
184
  uncoveredSegments: z.ZodNumber;
185
+ uncoveredDetail: z.ZodOptional<z.ZodArray<z.ZodObject<{
186
+ segment: z.ZodString;
187
+ reason: z.ZodEnum<{
188
+ redirection: "redirection";
189
+ no_rule_form: "no_rule_form";
190
+ cap_overflow: "cap_overflow";
191
+ }>;
192
+ }, z.core.$strip>>>;
125
193
  }, z.core.$strip>]>;
194
+ /** {@link RuleOfferRawSchema} 的输出形(结构已判、文本未限长)—— 素材铸点在它上面做派生一致性与座尺两道后判。 */
195
+ export type RuleOfferRaw = z.infer<typeof RuleOfferRawSchema>;
196
+ /**
197
+ * [ref] §3.5 —— `uncoveredDetail` 明细座的**座级**预筛,两条腿在 {@link RuleOfferRawSchema} 判形之前
198
+ * 各调一次。
199
+ *
200
+ * 为什么要在整体判形之前单独筛这一座:core 明写「ABSENCE(an older minter, a degraded consumer's re-emit)
201
+ * is not a claim」且 count 座是真源 —— 一座坏掉的明细(集外 reason / 非数组 / 与 count 不等 / 超帽)是
202
+ * **展示轴**上的损坏,正确处置是「丢这一座、留 batch 本体」;让它把整只 batch 判假会把一条真可兑的合取批
203
+ * 从卡上抹掉(方向反了:少一行解释是难看,少一只可兑的 offer 是把人的选项吃掉)。
204
+ * 座被丢**必须留痕**([ref]:F 类 fail-open 走 `recordFailOpen`,计数 + 一次 warn)。
205
+ * 只做浅层判别(不校验成员、不解构别的键),判形权威仍只有 zod 一处。
206
+ */
207
+ export declare function screenUncoveredDetailSeat(item: unknown): unknown;
208
+ /**
209
+ * [ref] B3 的**前一形**:pre-7.2 的 batch 成员没有 `kind`(那一形只有一种成员 —— 逐段 Bash 规则的四键),
210
+ * 而本仓的耐久腿在 park 行 `pendingAction.ruleOffers` 里**已经落了**这种成员(`checkpoint.rule_suggestions`
211
+ * 列),同步腿在 core 7.1.0 npm 字节下(本分支落 main 候 7.2.x)收到的也是它。B3 的降级臂说的是「`kind`
212
+ * 不识 ⇒ 丢整批」;一个**缺席**的 `kind` 不是「不识」——旧形恰好只有一种成员,缺席就是 `command`,零歧义。
213
+ * 所以这里做的是**已知旧形的 additive 回填**(浅层:仅当成员是对象、无 `kind` 键、且四个文本键在场时补
214
+ * `kind:"command"`),不是放宽闭集:带着一个陌生 `kind` 值的成员一个字节不动,照旧在 zod 处判假丢整批。
215
+ * 不回填的代价是真实的:滚动升级窗里,老二进制铸的每一条 park 行在新二进制的读面上整只 batch 消失
216
+ * (「有供给」谎报成「无供给」,本键存在理由的反面)。
217
+ */
218
+ export declare function backfillLegacyBatchMemberKind(item: unknown): unknown;
126
219
  /**
127
220
  * 卡面/帧面共用的 offer **投影型**(= {@link RuleOfferSchema} 的输出形)。判别位 `kind` 上闭集,
128
- * 消费端(SDK/cli 渲染)按 core 契约分臂;`batch.rules` 成员带 `segment` 渲染座(core 明写
129
- * 「渲染座、永不参与裁决」,与 rule/command 同为 UNTRUSTED-for-display)。
221
+ * 消费端(SDK/cli 渲染)按 core 契约分臂;`batch.rules` 成员是判别联合(`command` | `directoryRead`),
222
+ * 各自带 `segment` 渲染座(core 明写「渲染座、永不参与裁决」,与 rule/command/directory 同为
223
+ * UNTRUSTED-for-display);`uncoveredDetail` additive 明细座。
130
224
  */
131
225
  export type RuleOfferProjection = z.infer<typeof RuleOfferSchema>;
132
226
  /**
@@ -134,6 +228,11 @@ export type RuleOfferProjection = z.infer<typeof RuleOfferSchema>;
134
228
  * 分臂,抽成具名函数)。为什么不 `structuredClone`/展开:显式逐键投影是本模块对「上游 additive
135
229
  * 加键」的免疫层(见 {@link RuleOfferRawSchema} 顶注)——加键到不了 `card_json`,strip 语义在
136
230
  * 投影处兑现,不靠 schema 层兜。
231
+ *
232
+ * 到这里的素材已经过素材铸点的结构窄读(`tool-approval.ts` `boundRuleOfferTexts` 拿 {@link RuleOfferRawSchema}
233
+ * 判过形)⇒ 两级 `kind` 的运行期 `default` 臂在健康进程里不可达;它们守的是「本仓 dist 与 core 版本不同步」
234
+ * 那一形([ref] 词表范式:core 加臂 ⇒ `satisfies never` 编译红由人裁投影形;运行期撞到未知臂响亮抛,绝不把一个
235
+ * 不认识的臂当 batch/command 硬投)。
137
236
  */
138
237
  export declare function copyRuleOffer(o: RuleOffer): RuleOfferProjection;
139
238
  /**
@@ -242,20 +341,38 @@ export declare const ApprovalCardSchema: z.ZodObject<{
242
341
  match: z.ZodEnum<{
243
342
  exact: "exact";
244
343
  prefix: "prefix";
344
+ wildcard: "wildcard";
345
+ subpath: "subpath";
245
346
  }>;
246
347
  command: z.ZodString;
247
348
  }, z.core.$strict>, z.ZodObject<{
248
349
  kind: z.ZodLiteral<"batch">;
249
- rules: z.ZodArray<z.ZodObject<{
350
+ rules: z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
351
+ kind: z.ZodLiteral<"command">;
250
352
  rule: z.ZodString;
251
353
  match: z.ZodEnum<{
252
354
  exact: "exact";
253
355
  prefix: "prefix";
356
+ wildcard: "wildcard";
357
+ subpath: "subpath";
254
358
  }>;
255
359
  command: z.ZodString;
256
360
  segment: z.ZodString;
257
- }, z.core.$strict>>;
361
+ }, z.core.$strict>, z.ZodObject<{
362
+ kind: z.ZodLiteral<"directoryRead">;
363
+ rule: z.ZodString;
364
+ directory: z.ZodString;
365
+ segment: z.ZodString;
366
+ }, z.core.$strict>]>>;
258
367
  uncoveredSegments: z.ZodNumber;
368
+ uncoveredDetail: z.ZodOptional<z.ZodArray<z.ZodObject<{
369
+ segment: z.ZodString;
370
+ reason: z.ZodEnum<{
371
+ redirection: "redirection";
372
+ no_rule_form: "no_rule_form";
373
+ cap_overflow: "cap_overflow";
374
+ }>;
375
+ }, z.core.$strict>>>;
259
376
  }, z.core.$strict>]>>>;
260
377
  ruleOffersAbsence: z.ZodOptional<z.ZodEnum<{
261
378
  mandated: "mandated";
@@ -323,20 +440,38 @@ export declare const ApprovalCardEnvelopeSchema: z.ZodObject<{
323
440
  match: z.ZodEnum<{
324
441
  exact: "exact";
325
442
  prefix: "prefix";
443
+ wildcard: "wildcard";
444
+ subpath: "subpath";
326
445
  }>;
327
446
  command: z.ZodString;
328
447
  }, z.core.$strict>, z.ZodObject<{
329
448
  kind: z.ZodLiteral<"batch">;
330
- rules: z.ZodArray<z.ZodObject<{
449
+ rules: z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
450
+ kind: z.ZodLiteral<"command">;
331
451
  rule: z.ZodString;
332
452
  match: z.ZodEnum<{
333
453
  exact: "exact";
334
454
  prefix: "prefix";
455
+ wildcard: "wildcard";
456
+ subpath: "subpath";
335
457
  }>;
336
458
  command: z.ZodString;
337
459
  segment: z.ZodString;
338
- }, z.core.$strict>>;
460
+ }, z.core.$strict>, z.ZodObject<{
461
+ kind: z.ZodLiteral<"directoryRead">;
462
+ rule: z.ZodString;
463
+ directory: z.ZodString;
464
+ segment: z.ZodString;
465
+ }, z.core.$strict>]>>;
339
466
  uncoveredSegments: z.ZodNumber;
467
+ uncoveredDetail: z.ZodOptional<z.ZodArray<z.ZodObject<{
468
+ segment: z.ZodString;
469
+ reason: z.ZodEnum<{
470
+ redirection: "redirection";
471
+ no_rule_form: "no_rule_form";
472
+ cap_overflow: "cap_overflow";
473
+ }>;
474
+ }, z.core.$strict>>>;
340
475
  }, z.core.$strict>]>>>;
341
476
  ruleOffersAbsence: z.ZodOptional<z.ZodEnum<{
342
477
  mandated: "mandated";