@sema-agent/server 7.133.0 → 7.134.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 (185) hide show
  1. package/MIGRATION.md +87 -0
  2. package/README.md +2 -2
  3. package/README.zh-CN.md +2 -2
  4. package/USAGE.md +129 -11
  5. package/deploy/sema-up/README.md +16 -0
  6. package/deploy/sema-up/chart/Chart.yaml +1 -1
  7. package/deploy/sema-up/chart/templates/NOTES.txt +15 -0
  8. package/deploy/sema-up/chart/templates/_helpers.tpl +129 -0
  9. package/deploy/sema-up/chart/templates/sealed-key.yaml +24 -0
  10. package/deploy/sema-up/chart/templates/server-v4pro.yaml +14 -0
  11. package/deploy/sema-up/chart/templates/server.yaml +14 -0
  12. package/deploy/sema-up/chart/values-full-single.yaml +5 -1
  13. package/deploy/sema-up/chart/values.yaml +34 -0
  14. package/dist/approval-ask-audit-store.d.ts +1 -1
  15. package/dist/approval-card.d.ts +4 -4
  16. package/dist/approval-card.js +1 -2
  17. package/dist/approval-content-kind.d.ts +5 -2
  18. package/dist/approval-content-kind.js +16 -3
  19. package/dist/approval-gate-currency.d.ts +72 -0
  20. package/dist/approval-gate-currency.js +94 -0
  21. package/dist/approval.d.ts +6 -0
  22. package/dist/approval.js +31 -14
  23. package/dist/boot/coordinators.d.ts +6 -1
  24. package/dist/boot/coordinators.js +6 -3
  25. package/dist/boot/leader.d.ts +2 -0
  26. package/dist/boot/leader.js +2 -1
  27. package/dist/boot/read-deny-anchor-audit.js +2 -2
  28. package/dist/boot/runner-deps.d.ts +18 -5
  29. package/dist/boot/runner-deps.js +9 -0
  30. package/dist/boot/stage-06-runners.d.ts +4 -3
  31. package/dist/boot/stage-06-runners.js +15 -2
  32. package/dist/boot/stage-07-capability-layer.d.ts +2 -1
  33. package/dist/boot/stage-08-reapers.d.ts +2 -1
  34. package/dist/boot/stage-09-leader.d.ts +2 -1
  35. package/dist/boot/stage-09-leader.js +2 -2
  36. package/dist/boot/stage-10-http-server.d.ts +2 -1
  37. package/dist/boot/stage-10-http-server.js +2 -1
  38. package/dist/boot/stores.d.ts +8 -0
  39. package/dist/boot/stores.js +8 -12
  40. package/dist/boot/web-admin.d.ts +6 -0
  41. package/dist/boot/web-admin.js +11 -2
  42. package/dist/budget.js +5 -1
  43. package/dist/caller-text-echo.d.ts +2 -0
  44. package/dist/caller-text-echo.js +2 -1
  45. package/dist/canonical-json.d.ts +2 -41
  46. package/dist/canonical-json.js +2 -13
  47. package/dist/capabilities/composite-members.d.ts +1 -1
  48. package/dist/capabilities/composite-members.js +1 -1
  49. package/dist/capabilities/mcp-probe.js +2 -2
  50. package/dist/capability-facts.d.ts +2 -0
  51. package/dist/capability-facts.js +4 -4
  52. package/dist/checkpoint-ctx-seal-m28.d.ts +42 -0
  53. package/dist/checkpoint-ctx-seal-m28.js +19 -0
  54. package/dist/field-conflict.d.ts +2 -1
  55. package/dist/field-conflict.js +1 -0
  56. package/dist/file-history-disclosure.d.ts +34 -5
  57. package/dist/file-history-disclosure.js +2 -2
  58. package/dist/fleet/fleet-bus.d.ts +4 -0
  59. package/dist/fleet/fleet-bus.js +1 -0
  60. package/dist/hooks/hook-runner.js +2 -2
  61. package/dist/http/admission.js +8 -0
  62. package/dist/http/approval-sweeps.js +3 -1
  63. package/dist/http/checkpoint-ctx.d.ts +157 -2
  64. package/dist/http/checkpoint-ctx.js +158 -2
  65. package/dist/http/dispatch.js +3 -3
  66. package/dist/http/durable-run-leg.js +3 -2
  67. package/dist/http/idempotency.d.ts +2 -2
  68. package/dist/http/idempotency.js +2 -2
  69. package/dist/http/principal-gate.d.ts +32 -6
  70. package/dist/http/principal-gate.js +27 -4
  71. package/dist/http/refusal-body.d.ts +12 -0
  72. package/dist/http/refusal-body.js +11 -0
  73. package/dist/http/request-guards.d.ts +0 -9
  74. package/dist/http/request-guards.js +2 -16
  75. package/dist/http/resume-ctx-read.d.ts +48 -1
  76. package/dist/http/resume-ctx-read.js +26 -3
  77. package/dist/http/resume-legs.d.ts +2 -1
  78. package/dist/http/resume-legs.js +37 -22
  79. package/dist/http/route-ctx.d.ts +0 -9
  80. package/dist/http/routes/agents-roster.js +2 -3
  81. package/dist/http/routes/approvals-assistant.js +43 -9
  82. package/dist/http/routes/capabilities.js +1 -1
  83. package/dist/http/routes/diagnostics.d.ts +3 -3
  84. package/dist/http/routes/diagnostics.js +1 -1
  85. package/dist/http/routes/fleet.js +4 -4
  86. package/dist/http/routes/memory-origin.d.ts +1 -1
  87. package/dist/http/routes/rules.js +4 -4
  88. package/dist/http/routes/runs.js +30 -9
  89. package/dist/http/routes/session-sync.d.ts +3 -2
  90. package/dist/http/routes/session-sync.js +38 -20
  91. package/dist/http/routes/sessions-list.d.ts +1 -1
  92. package/dist/http/routes/sessions-list.js +3 -4
  93. package/dist/http/routes/sessions.js +4 -4
  94. package/dist/http/routes/tasks.js +21 -16
  95. package/dist/http/routes/trace-usage.js +7 -9
  96. package/dist/http/run-declared-families.d.ts +16 -0
  97. package/dist/http/run-declared-families.js +12 -0
  98. package/dist/http/server.d.ts +6 -0
  99. package/dist/http/server.js +5 -2
  100. package/dist/http/submit-ctx.d.ts +4 -3
  101. package/dist/http/submit-ctx.js +19 -3
  102. package/dist/http/wire-types.d.ts +7 -3
  103. package/dist/index.d.ts +2 -0
  104. package/dist/index.js +1 -0
  105. package/dist/leader/wire.d.ts +4 -0
  106. package/dist/leader/wire.js +8 -7
  107. package/dist/main.js +1 -1
  108. package/dist/memory-origin-fence.js +2 -2
  109. package/dist/model-compat.d.ts +1 -1
  110. package/dist/observability/prompt-manifest.d.ts +19 -8
  111. package/dist/observability/prompt-manifest.js +9 -8
  112. package/dist/observability/tool-trace.js +8 -9
  113. package/dist/orchestration/workflow-park-decision.d.ts +1 -1
  114. package/dist/orchestration/workflow-park-decision.js +1 -0
  115. package/dist/parked-decide.d.ts +4 -5
  116. package/dist/parked-decide.js +3 -1
  117. package/dist/permission-rule-vocab.d.ts +1 -23
  118. package/dist/permission-rule-vocab.js +0 -13
  119. package/dist/plugins/approval-payload-fingerprint.d.ts +14 -6
  120. package/dist/plugins/approval-payload-fingerprint.js +1 -1
  121. package/dist/plugins/caching-session-store.d.ts +1 -1
  122. package/dist/plugins/caching-session-store.js +4 -3
  123. package/dist/plugins/checkpoint-store-sql.d.ts +22 -1
  124. package/dist/plugins/checkpoint-store-sql.js +45 -1
  125. package/dist/plugins/local-checkpoint-store.d.ts +13 -2
  126. package/dist/plugins/local-checkpoint-store.js +26 -0
  127. package/dist/plugins/local-session-store.d.ts +12 -9
  128. package/dist/plugins/local-session-store.js +23 -14
  129. package/dist/plugins/local-session-window.d.ts +12 -6
  130. package/dist/plugins/local-session-window.js +1 -17
  131. package/dist/plugins/one-time-migrations.d.ts +11 -0
  132. package/dist/plugins/one-time-migrations.js +12 -0
  133. package/dist/plugins/permission-rule-store-file.js +10 -6
  134. package/dist/plugins/permission-rule-store-sql.js +8 -8
  135. package/dist/plugins/pg-session-storage.d.ts +1 -1
  136. package/dist/plugins/pg-session-storage.js +39 -17
  137. package/dist/plugins/remote-env-device.d.ts +14 -7
  138. package/dist/plugins/remote-env-device.js +23 -15
  139. package/dist/plugins/session-ownership.d.ts +11 -0
  140. package/dist/plugins/session-ownership.js +11 -0
  141. package/dist/plugins/session-placement.d.ts +12 -14
  142. package/dist/plugins/session-placement.js +2 -15
  143. package/dist/plugins/tidb-session-store.d.ts +1 -1
  144. package/dist/plugins/tidb-session-store.js +39 -17
  145. package/dist/request-tool-axes.d.ts +12 -0
  146. package/dist/request-tool-axes.js +20 -0
  147. package/dist/run-local.js +1 -0
  148. package/dist/runs.d.ts +2 -1
  149. package/dist/runs.js +4 -3
  150. package/dist/sealed-key.d.ts +32 -0
  151. package/dist/sealed-key.js +55 -1
  152. package/dist/security.d.ts +9 -7
  153. package/dist/session-error-wire.d.ts +12 -5
  154. package/dist/session-error-wire.js +4 -1
  155. package/dist/session-sync-kernel.d.ts +37 -10
  156. package/dist/session-sync-kernel.js +10 -0
  157. package/dist/task-a2a.js +4 -3
  158. package/dist/task-mcp.d.ts +2 -2
  159. package/dist/task-mcp.js +20 -8
  160. package/dist/task-settings.d.ts +5 -1
  161. package/dist/task-settings.js +1 -1
  162. package/dist/tool-approval.d.ts +151 -52
  163. package/dist/tool-approval.js +113 -20
  164. package/dist/tool-rule-names.d.ts +6 -0
  165. package/dist/trace/core-keyset-guard.d.ts +4 -3
  166. package/dist/trace/exit-seat.d.ts +13 -1
  167. package/dist/trace/exit-seat.js +55 -1
  168. package/dist/trace/ledger-events.d.ts +3 -1
  169. package/dist/trace/project.d.ts +18 -1
  170. package/dist/trace/project.js +16 -4
  171. package/dist/trace/redact.d.ts +6 -3
  172. package/dist/trace/redact.js +191 -23
  173. package/dist/trace/self-minted-wire.d.ts +4 -0
  174. package/dist/trace/self-minted-wire.js +1 -0
  175. package/dist/trace/wire-projection-faces.d.ts +6 -0
  176. package/dist/trace/wire-projection-faces.js +6 -0
  177. package/dist/web-admin-client/skills-mcp.d.ts +1 -1
  178. package/dist/web-admin-client/skills-mcp.js +2 -3
  179. package/dist/web-admin-client/types.d.ts +1 -1
  180. package/package.json +2 -2
  181. package/scripts/drain-check-local.mjs +34 -2
  182. package/scripts/drain-check.mysql.sql +20 -2
  183. package/scripts/drain-check.pg.sql +20 -2
  184. package/dist/mcp-tool-faces.d.ts +0 -69
  185. package/dist/mcp-tool-faces.js +0 -104
@@ -0,0 +1,129 @@
1
+ {{- /*
2
+ sealedKey 持久块的助手 —— 卷名 / 挂载路径 / 共享 pod 数 / 访问模式 / 耐久审批的渲值,各一处定义,主 worker(server.yaml)与
3
+ v4pro 变体(server-v4pro.yaml)同调,不各抄一份。
4
+
5
+ 这是什么:server 的数据根下 `sealed-key/` 目录存两样东西 —— 模型钥匙(web-admin 贴的 `sealedApiKey` 的解封私钥)与停驻任务续跑上下文里
6
+ 请求凭据的封签钥匙(子目录 `checkpoint-ctx-keys/`)。私钥只在这个目录里(不进库),所以它要**跨重启持久**、**每个副本是同一份**:
7
+ 不持久 ⇒ 每次重启换钥匙,重启前停驻、带请求凭据的任务续跑恒 409 conflict.checkpoint_ctx_unsealable(reason key_not_held);
8
+ 多副本各一份 ⇒ A 副本停驻的任务在 B 副本上同一个结局。
9
+
10
+ 路径:镜像以 root 跑、本图表不设 LOCAL_DATA_ROOT / CONFIG_LOCAL_DIR / AGENT_DATA_DIR ⇒ 数据根是缺省 ~/.ai-agent = /root/.ai-agent。
11
+ 给部署另设了数据根变量的人:挂载路径要跟着移(本图表不替部署设这几个变量)。
12
+ */ -}}
13
+
14
+ {{- /* 卷 / PVC 的名字。 */ -}}
15
+ {{- define "sema.sealedKey.claimName" -}}sema-server-sealed-key{{- end -}}
16
+
17
+ {{- /* 容器内挂载点 = <数据根>/sealed-key(整个目录:模型钥匙 + checkpoint-ctx-keys/ 同一卷)。 */ -}}
18
+ {{- define "sema.sealedKey.mountPath" -}}/root/.ai-agent/sealed-key{{- end -}}
19
+
20
+ {{- /*
21
+ 持久开关,且**只收布尔**:`--set-string ...enabled=false` / values 里带引号的 "false" 是字符串,模板里字符串恒为真 ⇒ 会静默保持持久开,
22
+ 与写的人的意思相反,所以非布尔渲染即拒。输出 "true" / "false"。
23
+ */ -}}
24
+ {{- define "sema.sealedKey.enabled" -}}
25
+ {{- $v := .Values.sealedKey.persistence.enabled -}}
26
+ {{- if not (kindIs "bool" $v) -}}
27
+ {{- fail (printf "sealedKey.persistence.enabled must be a boolean (true or false), got %s %q — a string such as \"false\" is truthy in a template and would silently keep the volume on" (kindOf $v) (toString $v)) -}}
28
+ {{- end -}}
29
+ {{- $v -}}
30
+ {{- end -}}
31
+
32
+ {{- /* 一只 Deployment 的副本数:缺省(null,k8s 按 1 只起)不当 0 只数;其余按整数。 */ -}}
33
+ {{- define "sema.sealedKey.replicasOf" -}}
34
+ {{- if kindIs "invalid" . -}}1{{- else -}}{{- int . -}}{{- end -}}
35
+ {{- end -}}
36
+
37
+ {{- /* 共享这只卷的 pod 数:主 worker 的副本数 + 启用的 v4pro 变体的副本数(两者读同一个库里的停驻行,所以挂同一只卷)。 */ -}}
38
+ {{- define "sema.sealedKey.podCount" -}}
39
+ {{- $n := int (include "sema.sealedKey.replicasOf" .Values.server.replicas) -}}
40
+ {{- if .Values.serverV4pro.enabled -}}
41
+ {{- $n = add $n (int (include "sema.sealedKey.replicasOf" .Values.serverV4pro.replicas)) -}}
42
+ {{- end -}}
43
+ {{- $n -}}
44
+ {{- end -}}
45
+
46
+ {{- /*
47
+ 访问模式:`sealedKey.persistence.accessMode` 留空 = 随共享 pod 数派生(一只 ReadWriteOnce,多只 ReadWriteMany —— 要集群的存储类支持 RWX,见 values.yaml sealedKey 块);
48
+ 写了 ReadWriteOnce / ReadWriteMany = 显式盖。显式盖的用处:单节点、多副本 —— 所有 pod 在同一个节点上,RWO 就够,不必为它给节点装 NFS 客户端;
49
+ 盖成 RWO 而 pod 其实会落在不同节点是写的人的前提(新 Pod 会卡在「卷被别的节点挂着」)。其它词渲染即拒。
50
+ */ -}}
51
+ {{- define "sema.sealedKey.accessMode" -}}
52
+ {{- $raw := .Values.sealedKey.persistence.accessMode -}}
53
+ {{- $m := "" -}}
54
+ {{- if not (kindIs "invalid" $raw) -}}
55
+ {{- $m = toString $raw -}}
56
+ {{- end -}}
57
+ {{- if not (has $m (list "" "ReadWriteOnce" "ReadWriteMany")) -}}
58
+ {{- fail (printf "sealedKey.persistence.accessMode must be empty (derive from the number of pods sharing the volume), \"ReadWriteOnce\" or \"ReadWriteMany\", got %q" $m) -}}
59
+ {{- end -}}
60
+ {{- if ne $m "" -}}
61
+ {{- $m -}}
62
+ {{- else if gt (int (include "sema.sealedKey.podCount" .)) 1 -}}
63
+ ReadWriteMany
64
+ {{- else -}}
65
+ ReadWriteOnce
66
+ {{- end -}}
67
+ {{- end -}}
68
+
69
+ {{- /*
70
+ Deployment 的更新策略:有效访问模式是 ReadWriteOnce ⇒ `Recreate`(`nindent 2` 接在 `replicas:` 之后)。RWO 卷同时只能被一个节点挂,默认的滚动更新先起新 Pod、
71
+ 旧 Pod 要等新 Pod 就绪才放卷 —— 新 Pod 落在别的节点就永远等不到卷。代价:升级窗口内这一只 Deployment 短暂没有副本(只有一只 pod 的形本来就没有别的副本可接)。
72
+ RWX 与持久关闭不碰策略。
73
+ */ -}}
74
+ {{- define "sema.sealedKey.strategy" -}}
75
+ {{- /* 访问模式先取(取值即校验):持久关闭时写了非闭集词也拒,「其它词渲染即拒」不看开关。 */ -}}
76
+ {{- $mode := include "sema.sealedKey.accessMode" . -}}
77
+ {{- if and (eq (include "sema.sealedKey.enabled" .) "true") (eq $mode "ReadWriteOnce") -}}
78
+ strategy: { type: Recreate }
79
+ {{- end -}}
80
+ {{- end -}}
81
+
82
+ {{- /*
83
+ `server.durableApproval`(耐久审批,env DURABLE_APPROVAL)渲给 worker 的值:"" = 不渲(开关归引擎按姿态定),"true" / "false" = 显式。
84
+ 布尔与字符串写法同义;其它词渲染即拒。持久被关(sealedKey.persistence.enabled=false)⇒ 固定 "false" —— 钥匙跨不过重启,耐久审批铸出来的停驻行
85
+ 就是续不了的行,所以图表替部署**显式**关掉它(不是靠引擎的缺省碰巧关);同一份 values 又把它点名成 true ⇒ 渲染即拒,两个键各说各的就是矛盾。
86
+ `kindIs "invalid"` 判 nil(`--set server.durableApproval=null` 删键):不能用 `default ""` —— 布尔 false 在 `default` 里算空,会把显式 false 吞成「没表态」。
87
+ */ -}}
88
+ {{- define "sema.durableApproval" -}}
89
+ {{- $raw := .Values.server.durableApproval -}}
90
+ {{- $want := "" -}}
91
+ {{- if not (kindIs "invalid" $raw) -}}
92
+ {{- $want = lower (trim (toString $raw)) -}}
93
+ {{- end -}}
94
+ {{- if not (has $want (list "" "true" "false")) -}}
95
+ {{- fail (printf "server.durableApproval must be \"true\", \"false\" or empty (empty = the engine decides by posture), got %q" $want) -}}
96
+ {{- end -}}
97
+ {{- if eq (include "sema.sealedKey.enabled" .) "true" -}}
98
+ {{- $want -}}
99
+ {{- else -}}
100
+ {{- if eq $want "true" -}}
101
+ {{- fail "server.durableApproval=true cannot be combined with sealedKey.persistence.enabled=false: durable approvals park tasks whose request credentials are sealed under keys kept in the sealed-key volume, and without that volume every restart replaces the keys so no parked task could resume. Keep sealedKey.persistence.enabled=true (the default), or drop server.durableApproval (with persistence off the chart sets DURABLE_APPROVAL=false itself)" -}}
102
+ {{- end -}}
103
+ false
104
+ {{- end -}}
105
+ {{- end -}}
106
+
107
+ {{- /* worker 容器 env 里的耐久审批一条(`nindent 12` 之后接在 env 列表里);值为空 ⇒ 什么都不出。 */ -}}
108
+ {{- define "sema.sealedKey.env" -}}
109
+ {{- with (include "sema.durableApproval" .) -}}
110
+ - { name: DURABLE_APPROVAL, value: {{ . | quote }} }
111
+ {{- end -}}
112
+ {{- end -}}
113
+
114
+ {{- /* worker 容器的 volumeMounts 块(`nindent 10`);持久关 ⇒ 什么都不出。 */ -}}
115
+ {{- define "sema.sealedKey.volumeMounts" -}}
116
+ {{- if eq (include "sema.sealedKey.enabled" .) "true" -}}
117
+ volumeMounts:
118
+ - { name: sealed-key, mountPath: {{ include "sema.sealedKey.mountPath" . | quote }} }
119
+ {{- end -}}
120
+ {{- end -}}
121
+
122
+ {{- /* Pod 的 volumes 块(`nindent 6`);持久关 ⇒ 什么都不出。 */ -}}
123
+ {{- define "sema.sealedKey.volumes" -}}
124
+ {{- if eq (include "sema.sealedKey.enabled" .) "true" -}}
125
+ volumes:
126
+ - name: sealed-key
127
+ persistentVolumeClaim: { claimName: {{ include "sema.sealedKey.claimName" . }} }
128
+ {{- end -}}
129
+ {{- end -}}
@@ -0,0 +1,24 @@
1
+ # ── 停驻封签钥匙的持久卷(sealedKey.persistence.enabled,缺省开)──────────────────────────
2
+ # server 的 <数据根>/sealed-key/ 整个目录(模型钥匙 + 子目录 checkpoint-ctx-keys/ 的停驻行封签钥匙)挂这一只 PVC,
3
+ # 主 worker 与启用的 v4pro 变体共用。私钥只在这里(不进库):卷丢了,封在停驻行里的请求凭据就开不开。
4
+ # 访问模式随共享这只卷的 pod 数(助手 sema.sealedKey.accessMode):一只 ReadWriteOnce,多只 ReadWriteMany(要集群的存储类支持 RWX)。
5
+ # ⚠ PVC 的 accessModes 建好后不可改:副本数跨过 1 的边界(1 ⇄ 多)会让升级撞「spec is immutable」—— 响亮,不静默;见 values.yaml sealedKey 块。
6
+ # 有效访问模式是 ReadWriteOnce 时 server / v4pro 两只 Deployment 用 Recreate(助手 sema.sealedKey.strategy)。
7
+ {{- if eq (include "sema.sealedKey.enabled" .) "true" }}
8
+ apiVersion: v1
9
+ kind: PersistentVolumeClaim
10
+ metadata:
11
+ name: {{ include "sema.sealedKey.claimName" . }}
12
+ namespace: {{ .Release.Namespace }}
13
+ annotations:
14
+ # 卷里是私钥:丢了,库里停驻行的请求凭据就开不开。`helm uninstall` 与「关持久」的 upgrade 都不顺手删它(确认不要了再 kubectl delete pvc)。
15
+ helm.sh/resource-policy: keep
16
+ spec:
17
+ accessModes: [{{ include "sema.sealedKey.accessMode" . }}]
18
+ {{- if .Values.sealedKey.persistence.storageClass }}
19
+ storageClassName: {{ .Values.sealedKey.persistence.storageClass | quote }}
20
+ {{- end }}
21
+ resources:
22
+ requests:
23
+ storage: {{ .Values.sealedKey.persistence.size | quote }}
24
+ {{- end }}
@@ -11,6 +11,9 @@ metadata:
11
11
  labels: { app: sema-server-v4pro }
12
12
  spec:
13
13
  replicas: {{ .Values.serverV4pro.replicas }}
14
+ {{- with (include "sema.sealedKey.strategy" .) }}
15
+ {{ . | indent 2 }}
16
+ {{- end }}
14
17
  selector:
15
18
  matchLabels: { app: sema-server-v4pro }
16
19
  template:
@@ -75,10 +78,21 @@ spec:
75
78
  - { name: K8S_SANDBOX_IMAGE, value: {{ .Values.sandbox.image | quote }} }
76
79
  - { name: K8S_SANDBOX_IPV6, value: {{ .Values.sandbox.ipv6 | quote }} }
77
80
  - { name: K8S_TOKEN, valueFrom: { secretKeyRef: { name: sema-runner-token-copy, key: token } } }
81
+ {{- /* 耐久审批的显式表态与停驻封签钥匙卷同源(助手 sema.durableApproval / sema.sealedKey.*,见 _helpers.tpl):关持久 ⇒ 显式 false;留空 ⇒ 不渲。 */}}
82
+ {{- with (include "sema.sealedKey.env" .) }}
83
+ {{ . | indent 12 }}
84
+ {{- end }}
78
85
  readinessProbe: # the only probe path is /readyz (/health is diagnostic, always 200)
79
86
  httpGet: { path: /readyz, port: 8090 }
80
87
  initialDelaySeconds: 5
81
88
  periodSeconds: 5
89
+ {{- /* <数据根>/sealed-key 的持久卷(整个目录:模型钥匙 + 停驻封签钥匙);关持久时两块都不出。 */}}
90
+ {{- with (include "sema.sealedKey.volumeMounts" .) }}
91
+ {{ . | indent 10 }}
92
+ {{- end }}
93
+ {{- with (include "sema.sealedKey.volumes" .) }}
94
+ {{ . | indent 6 }}
95
+ {{- end }}
82
96
  ---
83
97
  apiVersion: v1
84
98
  kind: Service
@@ -16,6 +16,9 @@ metadata:
16
16
  namespace: {{ .Release.Namespace }}
17
17
  spec:
18
18
  replicas: {{ .Values.server.replicas }}
19
+ {{- with (include "sema.sealedKey.strategy" .) }}
20
+ {{ . | indent 2 }}
21
+ {{- end }}
19
22
  selector:
20
23
  matchLabels: { app: sema-server }
21
24
  template:
@@ -88,10 +91,21 @@ spec:
88
91
  - { name: K8S_SANDBOX_IMAGE, value: {{ .Values.sandbox.image | quote }} }
89
92
  - { name: K8S_SANDBOX_IPV6, value: {{ .Values.sandbox.ipv6 | quote }} }
90
93
  - { name: K8S_TOKEN, valueFrom: { secretKeyRef: { name: sema-runner-token-copy, key: token } } }
94
+ {{- /* 耐久审批的显式表态与停驻封签钥匙卷同源(助手 sema.durableApproval / sema.sealedKey.*,见 _helpers.tpl):关持久 ⇒ 显式 false;留空 ⇒ 不渲。 */}}
95
+ {{- with (include "sema.sealedKey.env" .) }}
96
+ {{ . | indent 12 }}
97
+ {{- end }}
91
98
  readinessProbe: # the only probe path is /readyz (/health is diagnostic, always 200)
92
99
  httpGet: { path: /readyz, port: 8090 }
93
100
  initialDelaySeconds: 5
94
101
  periodSeconds: 5
102
+ {{- /* <数据根>/sealed-key 的持久卷(整个目录:模型钥匙 + 停驻封签钥匙);关持久时两块都不出。 */}}
103
+ {{- with (include "sema.sealedKey.volumeMounts" .) }}
104
+ {{ . | indent 10 }}
105
+ {{- end }}
106
+ {{- with (include "sema.sealedKey.volumes" .) }}
107
+ {{ . | indent 6 }}
108
+ {{- end }}
95
109
  ---
96
110
  apiVersion: v1
97
111
  kind: Service
@@ -1,5 +1,5 @@
1
1
  # full-single(单节点 k3s dev/测试)覆盖:1 实例 PG、preferred 反亲和(required 在单机=永久
2
- # Pending——真踩)、2 副本 server。用法:
2
+ # Pending——真踩)、2 副本 server(钥匙卷 RWO,见文末)。用法:
3
3
  # helm upgrade --install sema-stack ./chart -n sema --create-namespace \
4
4
  # -f chart/values-full-single.yaml --set server.image=… --set …(凭据)
5
5
  pg:
@@ -7,3 +7,7 @@ pg:
7
7
  antiAffinity: preferred
8
8
  server:
9
9
  replicas: 2
10
+ # 钥匙卷:单节点上的 2 个副本在同一个节点,ReadWriteOnce 的卷同节点可共用 —— 不必为它给节点装 NFS 客户端(缺省的 ReadWriteMany 要 RWX 存储类)。
11
+ sealedKey:
12
+ persistence:
13
+ accessMode: ReadWriteOnce
@@ -49,6 +49,10 @@ server:
49
49
  modelApiKey: "" # REQUIRED
50
50
  modelVision: "false"
51
51
  requirePrincipal: "false" # users=multi 时 true(身份隔离;compose 路径同名 env)
52
+ # 耐久审批(env DURABLE_APPROVAL;停驻 / 续跑 / /v1/approvals 整条路由)。留空 = 图表不渲这个 env,开关归引擎按姿态定
53
+ # (单用户 + 有耐久店 ⇒ 开;requirePrincipal=true 的多用户形 ⇒ 关);"true" / "false" = 显式表态,原样渲给每只 worker。
54
+ # sealedKey.persistence.enabled=false 时图表固定渲 "false"(钥匙跨不过重启,停驻行就续不了);此处再写 "true" ⇒ 渲染即拒。
55
+ durableApproval: ""
52
56
  # region=global 时置 "global"(B5 env-first);cn 留空 = 域内默认
53
57
  sandboxPkgSource: ""
54
58
  # k8s exec lane 的 TLS 姿势:true=渲染 K8S_INSECURE_TLS=true(chart 0.1.0 as-built);
@@ -66,6 +70,36 @@ server:
66
70
  s3PublicBucket: ""
67
71
  sendUserFileSandboxPutEndpoint: ""
68
72
 
73
+ # 停驻封签钥匙的持久卷:server 的 <数据根>/sealed-key/ 整个目录挂一只 PVC(缺省开)。
74
+ # 为什么:耐久审批(单用户 + 有耐久店时引擎缺省就开)停驻的任务,续跑上下文里请求自带的凭据只以封签落盘,封签的私钥只在这个目录里
75
+ # (子目录 checkpoint-ctx-keys/;同目录还有模型钥匙)——钥匙不进库。目录不持久 ⇒ 每次重启换一把钥匙;多副本各一份 ⇒ 换副本就换了钥匙。
76
+ # 两种结局相同:重启 / 换副本之前停驻、带请求凭据的任务续跑恒答 409 conflict.checkpoint_ctx_unsealable(reason key_not_held),只能作为新任务重提。
77
+ # 挂在哪:容器内 /root/.ai-agent/sealed-key(镜像以 root 跑、本图表不设数据根变量 ⇒ 数据根是缺省 /root/.ai-agent)。
78
+ # 主 worker 与启用的 serverV4pro 变体共用同一只 PVC(它们读同一个库里的停驻行)。
79
+ # 访问模式缺省随共享这只卷的 pod 数(server.replicas + 启用的 serverV4pro.replicas):1 只 ⇒ ReadWriteOnce;多只 ⇒ ReadWriteMany;`accessMode` 可显式盖(见下)。
80
+ # 有效访问模式是 ReadWriteOnce 时,server / serverV4pro 两只 Deployment 用 Recreate 更新策略(RWO 卷同时只能被一个节点挂,滚动更新的新 Pod 落在别的节点就永远等不到卷;
81
+ # 代价:升级窗口内这一只 Deployment 短暂没有副本)。
82
+ # ⚠ ReadWriteMany 要集群的存储类支持 RWX(Longhorn 的 RWX 走 NFS 共享管理器,每个节点要装 NFS 客户端 —— cluster-up.sh 不替你装;NFS / CephFS 类存储本就支持;
83
+ # local-path 与多数云盘类不支持,PVC 会一直 Pending、server Pod 起不来)。没有 RWX 的集群出路有三:server.replicas=1(RWO 即可);
84
+ # 单节点多副本(所有 pod 在同一个节点)显式 accessMode: ReadWriteOnce(随包的 values-full-single.yaml 就是这么写的);或关掉持久(下面 enabled: false,耐久审批随之关 —— 代价见下)。
85
+ # ⚠ PVC 的 accessModes 建好后不可改:副本数跨过 1 的边界(1 ⇄ 多;启用 v4pro 也算一只)时 helm upgrade 会撞 PVC「spec is immutable」—— 响亮失败,不静默;
86
+ # 这只卷里是私钥,不要为了过这个错直接删 PVC:先把停驻行结掉(随包排空检查脚本数一数)或备份钥匙文件,再换卷。
87
+ # PVC 带 helm.sh/resource-policy: keep:`helm uninstall` 与把 enabled 改成 false 的 upgrade 都不删它(钥匙留着;确认不要了再 `kubectl -n <ns> delete pvc sema-server-sealed-key`);
88
+ # 同名 release 重装会接管这只留下的 PVC。
89
+ # ⚠ 空卷上多副本并发首启:每个副本看到空目录各自生成一把钥匙,各副本只持有自己那一把,直到下次重启 —— 首装后 `kubectl -n <ns> rollout restart deploy/sema-server`
90
+ # 一次(启用 serverV4pro 时再加 deploy/sema-server-v4pro;NOTES 也说这句),重启之后每个副本读到目录里的全部钥匙,从此一致。
91
+ sealedKey:
92
+ persistence:
93
+ # true(缺省)= 建 PVC 并挂到上面的路径。
94
+ # false = 不建卷、不挂载,图表**显式**给每只 worker 设 DURABLE_APPROVAL=false(不产生续不了的停驻行;NOTES 也打印这一句);
95
+ # 此时 server.durableApproval 再写 "true" ⇒ 渲染即拒。代价:耐久审批整条关(审批卡停驻 / 续跑、/v1/approvals 路由、/v1/capabilities 的 approvals 位);
96
+ # web-admin 贴的模型钥匙 sealedApiKey 的私钥也跨不过重启(重启后那些条目答 stale_ciphertext,要对着新公钥重封)。
97
+ # 只收布尔:字符串 "false" 在模板里恒为真,会静默保持开着,所以渲染即拒。
98
+ enabled: true
99
+ size: 64Mi # 只放几个几百字节的钥匙文件;留余量给文件系统开销
100
+ storageClass: "" # 空 = 集群默认 SC;访问模式是 ReadWriteMany 时要点名一个支持它的类
101
+ accessMode: "" # 空 = 随共享 pod 数派生(1 只 RWO,多只 RWX);"ReadWriteOnce" / "ReadWriteMany" = 显式盖,其它词渲染即拒
102
+
69
103
  # v4-pro 变体 worker(2026-07-11 手工 kubectl 资源收编进 chart;同镜像/同 env 面,仅 MODEL_ID 不同;
70
104
  # 公网入口=Gcore LB listener 8091 → nodePort,LB 面不归 chart 管)
71
105
  serverV4pro:
@@ -23,7 +23,7 @@
23
23
  * - **不翻 `streamApproval` 能力位**([ref] 当时的裁 (c);⚠️ **这一位已由 S-384 翻真,不是被本店翻的**):
24
24
  * 当年不翻的理由是「该位在 SQL 车道隐含 durable park 兜底,local 给不了」+「local 的 ask 账是进程内
25
25
  * 易失形」。后半条已由 `FileApprovalAskStore` 消掉(持久账落地);前半条的答案一直在谓词里 ——
26
- * `resolveStreamApprovalGate` 的 `parkFacility` 合取项要求 `backend.checkpoint` 在场 ∧ `DURABLE_APPROVAL`,
26
+ * `resolveStreamApprovalGate` 的 park 设施(`checkpointStore`)合取项要求 `backend.checkpoint` 在场 ∧ `DURABLE_APPROVAL`,
27
27
  * 所以位报 true 的 local 部署**必然**配齐了 File 形 checkpoint 店(`durable-approval-local-e2e` 第一格
28
28
  * 实证:park 行跨双 kill 存活、重启后 decide 驱动 resume 真跑完)。本店与那一位自此无关:它治的是
29
29
  * **活卡窗内崩溃**(park 还没落盘)那一格的零痕迹病,而那一格无论位真位假都不复活 run。
@@ -717,8 +717,8 @@ export declare const ApprovalCardSchema: z.ZodObject<{
717
717
  }, z.core.$strict>>>;
718
718
  personalRuleDotsAbsent: z.ZodOptional<z.ZodCustom<AskEvidenceAbsence, AskEvidenceAbsence>>;
719
719
  }, z.core.$strict>>;
720
- askClass: z.ZodOptional<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "target_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "dangerous_delete" | "unverifiable_delete" | "unverifiable_shell_text" | "transcript_write" | "rule_unreadable" | "classifier_no_ruling" | "requires_user_interaction" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "target_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "dangerous_delete" | "unverifiable_delete" | "unverifiable_shell_text" | "transcript_write" | "rule_unreadable" | "classifier_no_ruling" | "requires_user_interaction" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary">>;
721
- classes: z.ZodOptional<z.ZodArray<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "target_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "dangerous_delete" | "unverifiable_delete" | "unverifiable_shell_text" | "transcript_write" | "rule_unreadable" | "classifier_no_ruling" | "requires_user_interaction" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "target_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "dangerous_delete" | "unverifiable_delete" | "unverifiable_shell_text" | "transcript_write" | "rule_unreadable" | "classifier_no_ruling" | "requires_user_interaction" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary">>>;
720
+ askClass: z.ZodOptional<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "target_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "dangerous_delete" | "unverifiable_delete" | "unverifiable_shell_text" | "transcript_write" | "rule_unreadable" | "classifier_no_ruling" | "requires_user_interaction" | "cross_boundary" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "target_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "dangerous_delete" | "unverifiable_delete" | "unverifiable_shell_text" | "transcript_write" | "rule_unreadable" | "classifier_no_ruling" | "requires_user_interaction" | "cross_boundary" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary">>;
721
+ classes: z.ZodOptional<z.ZodArray<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "target_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "dangerous_delete" | "unverifiable_delete" | "unverifiable_shell_text" | "transcript_write" | "rule_unreadable" | "classifier_no_ruling" | "requires_user_interaction" | "cross_boundary" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "target_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "dangerous_delete" | "unverifiable_delete" | "unverifiable_shell_text" | "transcript_write" | "rule_unreadable" | "classifier_no_ruling" | "requires_user_interaction" | "cross_boundary" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary">>>;
722
722
  fromSubagent: z.ZodOptional<z.ZodLiteral<true>>;
723
723
  sourceTaskId: z.ZodOptional<z.ZodString>;
724
724
  sourceAgentName: z.ZodOptional<z.ZodString>;
@@ -855,8 +855,8 @@ export declare const ApprovalCardEnvelopeSchema: z.ZodObject<{
855
855
  }, z.core.$strict>>>;
856
856
  personalRuleDotsAbsent: z.ZodOptional<z.ZodCustom<AskEvidenceAbsence, AskEvidenceAbsence>>;
857
857
  }, z.core.$strict>>;
858
- askClass: z.ZodOptional<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "target_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "dangerous_delete" | "unverifiable_delete" | "unverifiable_shell_text" | "transcript_write" | "rule_unreadable" | "classifier_no_ruling" | "requires_user_interaction" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "target_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "dangerous_delete" | "unverifiable_delete" | "unverifiable_shell_text" | "transcript_write" | "rule_unreadable" | "classifier_no_ruling" | "requires_user_interaction" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary">>;
859
- classes: z.ZodOptional<z.ZodArray<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "target_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "dangerous_delete" | "unverifiable_delete" | "unverifiable_shell_text" | "transcript_write" | "rule_unreadable" | "classifier_no_ruling" | "requires_user_interaction" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "target_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "dangerous_delete" | "unverifiable_delete" | "unverifiable_shell_text" | "transcript_write" | "rule_unreadable" | "classifier_no_ruling" | "requires_user_interaction" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary">>>;
858
+ askClass: z.ZodOptional<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "target_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "dangerous_delete" | "unverifiable_delete" | "unverifiable_shell_text" | "transcript_write" | "rule_unreadable" | "classifier_no_ruling" | "requires_user_interaction" | "cross_boundary" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "target_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "dangerous_delete" | "unverifiable_delete" | "unverifiable_shell_text" | "transcript_write" | "rule_unreadable" | "classifier_no_ruling" | "requires_user_interaction" | "cross_boundary" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary">>;
859
+ classes: z.ZodOptional<z.ZodArray<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "target_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "dangerous_delete" | "unverifiable_delete" | "unverifiable_shell_text" | "transcript_write" | "rule_unreadable" | "classifier_no_ruling" | "requires_user_interaction" | "cross_boundary" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "target_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "dangerous_delete" | "unverifiable_delete" | "unverifiable_shell_text" | "transcript_write" | "rule_unreadable" | "classifier_no_ruling" | "requires_user_interaction" | "cross_boundary" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary">>>;
860
860
  fromSubagent: z.ZodOptional<z.ZodLiteral<true>>;
861
861
  sourceTaskId: z.ZodOptional<z.ZodString>;
862
862
  sourceAgentName: z.ZodOptional<z.ZodString>;
@@ -1,10 +1,9 @@
1
1
  import { z } from "zod";
2
- import { ALLOW_RULE_MEMBERS, ASK_EVIDENCE_ABSENCE_VALUES, BOUND_GRANT_ABSENCES, MAX_RULE_TEXT_CHARS, READ_ROOT_CANDIDATE_DIR_MAX, RESUME_STARTUP_CAUSES, RULE_OFFERS_ABSENCES, STEER_REPLAY_FACETS, isAskClass, isAskOrigin, isAutoModeFallbackCause, isMintableProbeReason, isPersistedRuleMandate, screenAskClassMembers } from "@sema-agent/core";
2
+ import { ALLOW_RULE_MEMBERS, ASK_EVIDENCE_ABSENCE_VALUES, BOUND_GRANT_ABSENCES, MAX_RULE_TEXT_CHARS, PERSISTED_RULE_MATCHES, UNCOVERED_SEGMENT_REASONS, READ_ROOT_CANDIDATE_DIR_MAX, RESUME_STARTUP_CAUSES, RULE_OFFERS_ABSENCES, STEER_REPLAY_FACETS, isAskClass, isAskOrigin, isAutoModeFallbackCause, isMintableProbeReason, isPersistedRuleMandate, screenAskClassMembers } from "@sema-agent/core";
3
3
  import { redactSecrets } from "./trace/redact.js";
4
4
  import { recordFailOpen } from "./observability/fail-open.js";
5
5
  import { capText } from "./caller-text-echo.js";
6
6
  import { retireParkArmWords, retireStoredCardWords } from "./retired-card-words.js";
7
- import { PERSISTED_RULE_MATCHES, UNCOVERED_SEGMENT_REASONS } from "./permission-rule-vocab.js";
8
7
  export const MAX_AGENT_NAME = 200;
9
8
  const MAX_IDENT = 200;
10
9
  export const MAX_RULE_OFFERS = 4;
@@ -32,8 +32,11 @@ export interface ParkedGateFace {
32
32
  readonly approvalCard?: unknown;
33
33
  }
34
34
  /**
35
- * 停驻行的卡 = core `summarizeCheckpoint` 投给 `CheckpointSummary.approvalCard` 的那一个词:只在审批门(门型的寄存臂是
36
- * `tool_approval`,读 core `PAUSE_REGISTRY`)上读,词经 {@link readApprovalCard}。非审批门(计划审阅 / 资源 / 收尾)恒无卡。
35
+ * 停驻行的卡 —— 读法只有引擎那一只:core 包根导出 `parkedApprovalCardOf(gate)`(core 7.54.0 #1468 K2;审批门 = 门型的寄存臂是 `tool_approval`,
36
+ * 只在那里读卡,只有闭集成员算卡)。四态逐一处置(穷举 `switch`:core 加一态 ⇒ 编译红):`card` ⇒ 那个词;`no_card` / `not_approval` ⇒ 无卡;
37
+ * `unknown_word`(引擎交回**未经筛**的存储原值)⇒ 无卡 + 响亮:计一次 `server.approval-card.closed-word-out-of-set`(与 {@link readApprovalCard} 同一只计数、
38
+ * 同一只回显口径 —— 截 40、脱敏,对象只报形名)。修前本函数自己判「是不是审批门」(读 `PAUSE_REGISTRY` 的寄存臂)再交 {@link readApprovalCard},
39
+ * 与引擎的 `summarizeCheckpoint` 是两份读法。
37
40
  */
38
41
  export declare function parkedApprovalCard(gate: ParkedGateFace | undefined): ToolApprovalCard | undefined;
39
42
  /**
@@ -1,5 +1,5 @@
1
1
  export { ASK_USER_QUESTION_TOOL_NAME } from "@sema-agent/core";
2
- import { PAUSE_REGISTRY, TOOL_APPROVAL_CARDS, facetOf, isGateKind } from "@sema-agent/core";
2
+ import { TOOL_APPROVAL_CARDS, facetOf, isGateKind, parkedApprovalCardOf } from "@sema-agent/core";
3
3
  import { recordFailOpen } from "./observability/fail-open.js";
4
4
  import { redactSecrets } from "./trace/redact.js";
5
5
  export function readApprovalCard(raw) {
@@ -13,9 +13,22 @@ export function readApprovalCard(raw) {
13
13
  return undefined;
14
14
  }
15
15
  export function parkedApprovalCard(gate) {
16
- if (gate === undefined || !isGateKind(gate.kind) || PAUSE_REGISTRY[gate.kind].pending !== "tool_approval")
16
+ if (gate === undefined)
17
17
  return undefined;
18
- return readApprovalCard(gate.approvalCard);
18
+ const reading = parkedApprovalCardOf({ kind: gate.kind, approvalCard: gate.approvalCard });
19
+ switch (reading.kind) {
20
+ case "card":
21
+ return reading.card;
22
+ case "no_card":
23
+ case "not_approval":
24
+ return undefined;
25
+ case "unknown_word":
26
+ return readApprovalCard(reading.word);
27
+ default: {
28
+ const unreachable = reading;
29
+ throw new Error(`unhandled parked approval card reading: ${JSON.stringify(unreachable)}`);
30
+ }
31
+ }
19
32
  }
20
33
  export function parkedFacetOf(gate) {
21
34
  return gate !== undefined && isGateKind(gate.kind) ? facetOf({ kind: gate.kind, approvalCard: gate.approvalCard }) : undefined;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * 一张停驻审批卡的「门」此刻还是不是它 —— 应答口判 `approval_stale` 与按问读口 `state` 的**同一只判官**。
3
+ *
4
+ * ## 病灶(为什么要有这一只)
5
+ * 问账行 `PARKED` 是终态(`approval-ask-machine.ts`:`PARKED: []`):赎回腿的单赢者是 **checkpoint 的 CAS**,取消腿动的也是 checkpoint(先 `expire` 门、再驱 run 终局),
6
+ * 两条腿都不写 ask 行。于是「这张卡的门还在不在」这件事只有 checkpoint 店知道;读口若只看行,就会在卡被答过 / run 被取消之后继续说 `parked` 并指路去决定,
7
+ * 而应答口对同一张卡早已答 `409 approval_stale`。同一件事两个读法 —— 本模块把它收成一个:读的是同一个问题、同一个来源。
8
+ *
9
+ * ## 判官
10
+ * 问题:**这把 gate token 是不是该会话当前待决的审批门?**(应答口的答法:`resumeCheckpoint` 的 stale 臂 —— 回显的 token ≠ 该会话当前待决的审批门 ⇒ `409 approval_stale`。)
11
+ * · {@link currentApprovalGateToken} —— 该会话当前待决的审批门(审批门型优先,没有再退到任一待决行);应答口读它,读口读它,别处不再内联第二份;
12
+ * · {@link readGateCurrency} —— 一把 token 对着它的读数:`current` / `absent`(店里没有这一行)/ `settled`(行在,但不再是当前待决门:已结算、已过期、被取代);
13
+ * · {@link createParkedGateRead} —— 协调器读口的门读席:把读数译成读口要的四种读法({@link ParkedGateReading}),`state` 闭集不加字。
14
+ *
15
+ * ## `settled` 怎么译(读口只说 checkpoint 上有凭据的话;一条规矩)
16
+ * 落盘的行不受类型面约束(旧版 / 手改 / 坏体 / 更新版本 core 写下的行可带闭集外的词或坏形),所以已结算门只按**一条规矩**读:
17
+ * ① 两个闭集词(行的 `status`、结算记录的种类 `settlement.kind`)各查一张穷举表,**同一查法**({@link throughTable}:只认表自己的键,非字符串不进表):
18
+ * 表明说 `decided` ⇒ 往下读;明说 `void` ⇒ `void`(没有人决过它,答案无处兑现 —— 已过期〔取消 / 收割〕、仍待决却不是当前门〔被取代,应答口答 stale〕、
19
+ * 结算种类是引擎窗到期 / 宿主 SLA 扫 / 无人可答的失败族);**表外 ⇒ `unknown`**(读不懂的词撑不起「有人决过」,也撑不起「没有人决过它」);
20
+ * ② 结算赢家的门记录 `resolvedOutcome.gateOutcome` 先过 core 唯一的门记录筛 `screenGateOutcome`(**档案读法**:读回的耐久行,闭集 = 今日成员 ∪ 已退役词;
21
+ * 坏形记缺陷、绝不抛;本仓 `trace/project.ts` 投影同一种记录也只经这一只筛),有缺陷 ⇒ `unknown`,过了筛才读它的成员;
22
+ * ③ 分类之前记录要**读得全**:有结算记录、时刻(引擎写进赢家的 `settlement.when`)是有限数、赢家绑的是这一行的调用且判词是 allow / deny(与
23
+ * `findDecidedApprovalsForBinding` 同一只 {@link approvalDecisionOfWinner})—— 缺一个都是 `unknown`(**不编一个首决**;读口答 404,应答口对已结算的门答 409 `approval_stale`,两口不同判;
24
+ * `void` 也要读得全:绑在别的调用上的结算说的不是这张卡);读全了再按**结算种类**表分类:人 / 策略的四个种类 ⇒ `decided`(理由取自赢家的 `reason`),其余十个 ⇒ `void`
25
+ * (按种类不按当事方:跨版本稳定的是种类词,变过义的是当事方词 —— 此前版本的策略拒记 `policy_refused` + `who.party:"none"`,此后记 `"policy"`,按当事方查会把那一代合法历史记录读成 `void`);
26
+ * ④ 读的只是行的内容、不碰店:内容坏到读的时候抛(行上缺了必填成员)同样是坏行 ⇒ `unknown`,不冒成店故障(读口对店故障退回 `parked`)。
27
+ * `unknown` 的这一类(`decision_unreadable`)带上读不出的那几处(缺陷句,非空):读口答 404 `tool_approval.not_pending`,并记一行 error(首句 + 计数)。
28
+ * 行没了 ⇒ `unknown`(`row_absent`;应答口 PARKED 臂的 404 `not_found.approval` 同判)。
29
+ *
30
+ * ## 盲区(core 的筛不判的两处,如实写下:一处本判官手判有限数,一处本判官也不核)
31
+ * · 结算时刻 `settlement.when`:筛不判,本判官手判「有限数」;而本仓另一个读同一种记录的读者(trace 投影)把 `when` 原样放上 wire —— 同一个坏时刻,两个读者两种答法;
32
+ * · 方向与配对:赢家的方向(`resolvedOutcome.decision`)与记录自己的种类 / 处置说的方向是否一致,以及种类 × 当事方的配对 —— 筛不判,本判官也不核:读口以赢家的方向为准、读的是种类。
33
+ * 只有坏行 / 手改行才走得到这两处。候引擎的筛把时刻、方向与种类 × 当事方配对一并判了,两个读者同时退掉各自的手判(配对今天没有手判可退 —— 它无人判)。
34
+ */
35
+ import { type Checkpoint, type CheckpointToken, type SettlementKind } from "@sema-agent/core";
36
+ import { type PendingRowBinding } from "./plugins/checkpoint-store-sql.js";
37
+ import { type ParkedGateRead } from "./tool-approval.js";
38
+ /** 判官要的两个店口(三后端同形;窄接口让测试与装配都不必拿整只店)。 */
39
+ export interface GateCurrencyStore {
40
+ get(token: CheckpointToken): Promise<Checkpoint | null>;
41
+ findPendingTokenBySession(sessionId: string, scope?: string, binding?: PendingRowBinding): Promise<CheckpointToken | null | undefined>;
42
+ }
43
+ /**
44
+ * 该会话**当前待决的审批门**的 token:审批门型({@link APPROVAL_GATE_KINDS})优先 —— 同会话 `task_done` 纯 park 与新 run 的审批 park 可以并存(表无 per-session pending 唯一约束),
45
+ * 无序读会选中别的门型 —— 没有审批门才退到任一待决行(错门型诊断由 `/decide` 的 gate-kind 守卫点名)。应答口的 stale 臂与按问读口读这一只。
46
+ */
47
+ export declare function currentApprovalGateToken(cs: Pick<GateCurrencyStore, "findPendingTokenBySession">, sessionId: string): Promise<CheckpointToken | null | undefined>;
48
+ /** 一把 gate token 对着判官的读数。 */
49
+ export type GateCurrency = {
50
+ readonly kind: "current";
51
+ } | {
52
+ readonly kind: "absent";
53
+ } | {
54
+ readonly kind: "settled";
55
+ readonly checkpoint: Checkpoint;
56
+ };
57
+ /** 这把 gate token 是不是它那条会话当前待决的审批门。读不出(店抛错)⇒ 原样上抛,由调用方的故障姿势处置。 */
58
+ export declare function readGateCurrency(cs: GateCurrencyStore, gateToken: string): Promise<GateCurrency>;
59
+ /** 一个闭集词对已结算门的读法类:有人 / 有策略决过(往下读决议),或没有人决过它。 */
60
+ type SettledClass = "decided" | "void";
61
+ /**
62
+ * 结算种类(core `SETTLEMENT_KINDS`)→ 这次结算算不算「有人 / 有策略决过」。`human_*`(人)与 `policy_*`(部署的策略)四个词是仅有的决议种类;其余十个 ——
63
+ * 引擎自己的窗到期 · 宿主窗 / 宿主 SLA 扫 · 无人可答的失败族 —— 没有人决过它,不读成「决议:deny」(那会让壳把一次宿主超时清算渲成「被拒绝」)。
64
+ * 表的键是**种类**不是当事方:种类词跨版本稳定,当事方词变过义(此前版本的策略拒记 `who.party:"none"`),按当事方查会把那一代合法历史记录读错。core 现成的两张种类表
65
+ * (是不是拒绝 / 有没有人评判过)答的是别的问题:`human_refused` 与 `approval_window_expired` 同是拒绝,这里一个是决议、一个不是,所以不从它们派生。
66
+ * 表穷举(与上面 `status` 表同形、同一查法):core 给种类闭集加词而不在此表态 ⇒ 编译红。
67
+ */
68
+ export declare const SETTLEMENT_KIND_READING: Record<SettlementKind, SettledClass>;
69
+ /** 协调器读口的门读席({@link ParkedGateRead}):装配点把 checkpoint 店递进来,协调器本身不认识 checkpoint 的形。 */
70
+ export declare function createParkedGateRead(cs: GateCurrencyStore): ParkedGateRead;
71
+ export {};
72
+ //# sourceMappingURL=approval-gate-currency.d.ts.map
@@ -0,0 +1,94 @@
1
+ import { screenGateOutcome } from "@sema-agent/core";
2
+ import { approvalDecisionOfWinner } from "./plugins/checkpoint-store-sql.js";
3
+ import { APPROVAL_GATE_KINDS } from "./tool-approval.js";
4
+ export async function currentApprovalGateToken(cs, sessionId) {
5
+ return (await cs.findPendingTokenBySession(sessionId, undefined, { gateKinds: APPROVAL_GATE_KINDS })) ?? (await cs.findPendingTokenBySession(sessionId));
6
+ }
7
+ export async function readGateCurrency(cs, gateToken) {
8
+ const first = await cs.get(gateToken);
9
+ if (first === null)
10
+ return { kind: "absent" };
11
+ if (first.status !== "pending")
12
+ return { kind: "settled", checkpoint: first };
13
+ const current = await currentApprovalGateToken(cs, first.sessionId);
14
+ if (current === gateToken)
15
+ return { kind: "current" };
16
+ const fresh = await cs.get(gateToken);
17
+ return fresh === null ? { kind: "absent" } : { kind: "settled", checkpoint: fresh };
18
+ }
19
+ const SETTLED_GATE_READING = { resolved: "decided", expired: "void", pending: "void" };
20
+ export const SETTLEMENT_KIND_READING = {
21
+ human_allowed: "decided",
22
+ human_refused: "decided",
23
+ policy_allowed: "decided",
24
+ policy_refused: "decided",
25
+ approval_window_expired: "void",
26
+ denial_limit_window_expired: "void",
27
+ park_sla_expired: "void",
28
+ no_approver: "void",
29
+ approver_unavailable: "void",
30
+ approver_error: "void",
31
+ approver_contract: "void",
32
+ presentation_failed: "void",
33
+ blanket_allow_refused: "void",
34
+ task_aborted: "void",
35
+ };
36
+ function unreadable(defects) {
37
+ return { kind: "unknown", why: "decision_unreadable", defects };
38
+ }
39
+ function throughTable(table, word, field) {
40
+ if (typeof word !== "string" || !Object.hasOwn(table, word))
41
+ return unreadable([`${field} has no reading in this server's table of its closed set`]);
42
+ return table[word] === "decided" ? "decided" : { kind: "void" };
43
+ }
44
+ function settledReading(checkpoint, gateBoundCallId) {
45
+ try {
46
+ return readSettledRow(checkpoint, gateBoundCallId);
47
+ }
48
+ catch (e) {
49
+ return unreadable([`the settled row could not be read (${e instanceof Error ? e.name : "non-Error throw"})`]);
50
+ }
51
+ }
52
+ function readSettledRow(checkpoint, gateBoundCallId) {
53
+ const byStatus = throughTable(SETTLED_GATE_READING, checkpoint.status, "checkpoint status");
54
+ if (byStatus !== "decided")
55
+ return byStatus;
56
+ const winner = checkpoint.resolvedOutcome;
57
+ const recorded = winner?.gateOutcome;
58
+ const [defect, ...more] = screenGateOutcome(recorded, { reading: "archive" });
59
+ if (defect !== undefined)
60
+ return unreadable([defect, ...more]);
61
+ const settlement = recorded?.settlement;
62
+ if (settlement === undefined)
63
+ return unreadable(["the gate record carries no settlement: no decider and no time"]);
64
+ if (!Number.isFinite(settlement.when))
65
+ return unreadable(["settlement.when is not a finite epoch-ms time"]);
66
+ const boundCallId = gateBoundCallId ?? (checkpoint.pendingAction.kind === "tool_approval" ? checkpoint.pendingAction.toolCallId : "");
67
+ const direction = approvalDecisionOfWinner(winner, boundCallId);
68
+ if (direction === null)
69
+ return unreadable([`the resolved winner is not an allow / deny bound to call "${boundCallId}"`]);
70
+ const byKind = throughTable(SETTLEMENT_KIND_READING, settlement.kind, "settlement.kind");
71
+ if (byKind !== "decided")
72
+ return byKind;
73
+ const note = typeof winner?.reason === "string" && winner.reason !== "" ? winner.reason : undefined;
74
+ return { kind: "decided", decision: direction === "approve" ? "allow" : "deny", decidedAtMs: settlement.when, ...(note !== undefined ? { decisionNote: note } : {}) };
75
+ }
76
+ export function createParkedGateRead(cs) {
77
+ return async ({ gateToken, gateBoundCallId }) => {
78
+ const currency = await readGateCurrency(cs, gateToken);
79
+ switch (currency.kind) {
80
+ case "current":
81
+ return { kind: "open" };
82
+ case "absent":
83
+ return { kind: "unknown", why: "row_absent" };
84
+ case "settled":
85
+ return settledReading(currency.checkpoint, gateBoundCallId);
86
+ default: {
87
+ const unhandled = currency;
88
+ void unhandled;
89
+ return { kind: "unknown", why: "row_absent" };
90
+ }
91
+ }
92
+ };
93
+ }
94
+ //# sourceMappingURL=approval-gate-currency.js.map
@@ -184,6 +184,12 @@ export declare function createDeploymentListExemption(store: Pick<ApprovalExempt
184
184
  info(event: string, fields?: Record<string, unknown>): void;
185
185
  warn(event: string, fields?: Record<string, unknown>): void;
186
186
  }, leg?: "parked_revive"): DeploymentListExemption;
187
+ /**
188
+ * **每腿自动批额度的审批人名**(本仓闭词;core `AskOutcome.approver` 契约逐字「a closed word the host owns, never a person」)。
189
+ * 结算记录读 `policy_allowed` / `who.party: "policy"` / `who.approver` = 本词 —— 审计读者按它认出「这一次放行是部署的自动批额度答的」,
190
+ * 与无人值守腿的 `unattended_class_answer`(`unattended-permission-mode.ts`)同族。
191
+ */
192
+ export declare const AUTO_APPROVE_BUDGET_APPROVER = "auto_approve_budget";
187
193
  /**
188
194
  * **请求叶策略:`settings.permissions.neverAuto`**(S-1351)—— 命中名单的工具恒问真人;名单外「本层没有意见」(裸 allow)。叶只管请求自己写的那张表,部署的 `APPROVAL_NEVER_AUTO`
189
195
  * 在基线里({@link createDurableAskPolicy}),两者经 `tightenGoverned` 的 deny-wins 折叠取并:请求叶只会**多**问,删不掉基线里的任何一行。