@sema-agent/server 7.131.0 → 7.132.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 (256) hide show
  1. package/MIGRATION.md +309 -0
  2. package/README.md +5 -1
  3. package/README.zh-CN.md +3 -1
  4. package/USAGE.md +163 -54
  5. package/dist/approval-card.d.ts +3 -1
  6. package/dist/approval-card.js +2 -3
  7. package/dist/audit.d.ts +7 -10
  8. package/dist/auth-bridge.d.ts +4 -0
  9. package/dist/auth-bridge.js +10 -11
  10. package/dist/boot/deployment-mods.d.ts +31 -0
  11. package/dist/boot/deployment-mods.js +26 -0
  12. package/dist/boot/execution-env.d.ts +1 -1
  13. package/dist/boot/execution-env.js +19 -6
  14. package/dist/boot/memory-boundary.d.ts +2 -2
  15. package/dist/boot/read-deny-anchor-audit.d.ts +17 -0
  16. package/dist/boot/read-deny-anchor-audit.js +36 -0
  17. package/dist/boot/resolve-spec.d.ts +1 -5
  18. package/dist/boot/resolve-spec.js +19 -47
  19. package/dist/boot/runner-deps.d.ts +19 -8
  20. package/dist/boot/runner-deps.js +14 -2
  21. package/dist/boot/session-faces.d.ts +1 -7
  22. package/dist/boot/stage-03-store-backend.d.ts +1 -1
  23. package/dist/boot/stage-03-store-backend.js +2 -2
  24. package/dist/boot/stage-04-stores.d.ts +1 -1
  25. package/dist/boot/stage-05-execution-env.d.ts +4 -4
  26. package/dist/boot/stage-05-execution-env.js +7 -10
  27. package/dist/boot/stage-06-runners.d.ts +2 -2
  28. package/dist/boot/stage-06-runners.js +5 -4
  29. package/dist/boot/stage-07-capability-layer.d.ts +2 -2
  30. package/dist/boot/stage-07-capability-layer.js +3 -3
  31. package/dist/boot/stage-08-reapers.d.ts +2 -2
  32. package/dist/boot/stage-09-leader.d.ts +3 -9
  33. package/dist/boot/stage-10-http-server.d.ts +3 -9
  34. package/dist/boot/stage-10-http-server.js +6 -7
  35. package/dist/boot/stores.d.ts +1 -1
  36. package/dist/boot/stores.js +5 -6
  37. package/dist/boot/web-admin.d.ts +0 -3
  38. package/dist/boot/web-admin.js +35 -35
  39. package/dist/brain.js +17 -22
  40. package/dist/budget.js +5 -4
  41. package/dist/caller-text-echo.d.ts +20 -0
  42. package/dist/caller-text-echo.js +24 -0
  43. package/dist/capabilities/brand-identity.d.ts +26 -0
  44. package/dist/capabilities/brand-identity.js +27 -0
  45. package/dist/capabilities/center-plugins.js +1 -1
  46. package/dist/capabilities/center-prompts.d.ts +32 -59
  47. package/dist/capabilities/center-prompts.js +18 -32
  48. package/dist/capabilities/engine-capabilities.js +2 -1
  49. package/dist/capabilities/hands-lane.d.ts +9 -6
  50. package/dist/capabilities/hands-lane.js +1 -1
  51. package/dist/capabilities/mcp-probe.d.ts +14 -1
  52. package/dist/capabilities/mcp-probe.js +22 -133
  53. package/dist/capabilities/scenarios.d.ts +34 -40
  54. package/dist/capabilities/scenarios.js +16 -33
  55. package/dist/capabilities/skills.d.ts +19 -4
  56. package/dist/capabilities/skills.js +30 -10
  57. package/dist/config-catalog.js +6 -6
  58. package/dist/config-lkg.d.ts +9 -9
  59. package/dist/config-lkg.js +20 -4
  60. package/dist/config-provider.d.ts +3 -1
  61. package/dist/config-provider.js +12 -4
  62. package/dist/config-types.d.ts +7 -9
  63. package/dist/config.js +17 -4
  64. package/dist/device-ws-protocol.d.ts +1 -1
  65. package/dist/device-ws-protocol.js +1 -0
  66. package/dist/env-name-allowlist-knobs.d.ts +12 -2
  67. package/dist/env-name-allowlist-knobs.js +5 -4
  68. package/dist/exec-bounds.d.ts +15 -3
  69. package/dist/exec-bounds.js +4 -0
  70. package/dist/execution-lane-caps.d.ts +9 -2
  71. package/dist/execution-lane-caps.js +11 -0
  72. package/dist/hooks/classic-stations.d.ts +86 -0
  73. package/dist/hooks/classic-stations.js +27 -0
  74. package/dist/hooks/hook-llm.d.ts +8 -5
  75. package/dist/hooks/hook-llm.js +78 -35
  76. package/dist/hooks/hook-runner.d.ts +232 -80
  77. package/dist/hooks/hook-runner.js +218 -312
  78. package/dist/http/admission.d.ts +8 -2
  79. package/dist/http/admission.js +101 -57
  80. package/dist/http/approval-sweeps.js +57 -16
  81. package/dist/http/checkpoint-ctx.d.ts +25 -8
  82. package/dist/http/checkpoint-ctx.js +11 -7
  83. package/dist/http/claimed-run.d.ts +13 -0
  84. package/dist/http/claimed-run.js +15 -0
  85. package/dist/http/device-placement.js +5 -10
  86. package/dist/http/dispatch.d.ts +0 -4
  87. package/dist/http/dispatch.js +5 -9
  88. package/dist/http/durable-run-leg.d.ts +2 -1
  89. package/dist/http/durable-run-leg.js +5 -3
  90. package/dist/http/health-self-report.d.ts +7 -5
  91. package/dist/http/health-self-report.js +4 -18
  92. package/dist/http/principal-gate.d.ts +23 -6
  93. package/dist/http/principal-gate.js +24 -5
  94. package/dist/http/refusal-body.d.ts +21 -0
  95. package/dist/http/refusal-body.js +4 -0
  96. package/dist/http/request-guards.d.ts +27 -0
  97. package/dist/http/request-guards.js +24 -3
  98. package/dist/http/resume-legs.d.ts +25 -0
  99. package/dist/http/resume-legs.js +43 -22
  100. package/dist/http/routes/capabilities.js +1 -1
  101. package/dist/http/routes/diagnostics.js +1 -1
  102. package/dist/http/routes/rules.js +2 -2
  103. package/dist/http/routes/runs.js +7 -7
  104. package/dist/http/routes/session-sync.js +4 -2
  105. package/dist/http/routes/sessions.js +1 -1
  106. package/dist/http/routes/shared-memory.js +2 -13
  107. package/dist/http/routes/side-query.js +2 -3
  108. package/dist/http/routes/tasks.js +19 -9
  109. package/dist/http/send.d.ts +9 -7
  110. package/dist/http/send.js +7 -2
  111. package/dist/http/server.d.ts +21 -11
  112. package/dist/http/server.js +28 -2
  113. package/dist/http/submit-ctx.d.ts +25 -0
  114. package/dist/http/submit-ctx.js +39 -0
  115. package/dist/http/wire-types.d.ts +1 -1
  116. package/dist/index.d.ts +1 -1
  117. package/dist/index.js +1 -1
  118. package/dist/lane-home-probe.d.ts +43 -0
  119. package/dist/lane-home-probe.js +35 -0
  120. package/dist/limit-refusal.d.ts +47 -0
  121. package/dist/limit-refusal.js +12 -0
  122. package/dist/main.js +1 -1
  123. package/dist/mcp-assertion-header.d.ts +1 -9
  124. package/dist/mcp-assertion-header.js +0 -6
  125. package/dist/mcp-tool-faces.d.ts +5 -0
  126. package/dist/mcp-tool-faces.js +5 -1
  127. package/dist/memory-scope.d.ts +1 -1
  128. package/dist/model-provider.d.ts +39 -12
  129. package/dist/model-provider.js +19 -2
  130. package/dist/model-route-endpoint.d.ts +1 -1
  131. package/dist/model-route-endpoint.js +5 -5
  132. package/dist/observability/cost-taxonomy.d.ts +7 -7
  133. package/dist/observability/fail-open.d.ts +42 -6
  134. package/dist/observability/fail-open.js +42 -6
  135. package/dist/observability/metrics.js +3 -1
  136. package/dist/observability/prompt-manifest.d.ts +47 -82
  137. package/dist/observability/prompt-manifest.js +119 -17
  138. package/dist/observability/run-terminal.js +14 -6
  139. package/dist/observability/secret-env-scrub.d.ts +13 -1
  140. package/dist/observability/secret-env-scrub.js +13 -1
  141. package/dist/observability/tool-trace.d.ts +16 -21
  142. package/dist/observability/tool-trace.js +51 -42
  143. package/dist/per-task-image.d.ts +7 -1
  144. package/dist/per-task-image.js +3 -3
  145. package/dist/plain-string-record.d.ts +25 -0
  146. package/dist/plain-string-record.js +32 -0
  147. package/dist/plugins/blob-backend.d.ts +68 -13
  148. package/dist/plugins/blob-backend.js +53 -10
  149. package/dist/plugins/checkpoint-store-sql.d.ts +14 -4
  150. package/dist/plugins/checkpoint-store-sql.js +39 -26
  151. package/dist/plugins/k8s-bg-scripts.d.ts +11 -0
  152. package/dist/plugins/k8s-bg-scripts.js +22 -0
  153. package/dist/plugins/local-checkpoint-store.js +5 -2
  154. package/dist/plugins/local-session-store.js +1 -1
  155. package/dist/plugins/one-time-migrations.d.ts +137 -90
  156. package/dist/plugins/one-time-migrations.js +356 -207
  157. package/dist/plugins/permission-rule-store-sql.d.ts +13 -7
  158. package/dist/plugins/permission-rule-store-sql.js +4 -18
  159. package/dist/plugins/pg-session-storage.d.ts +4 -5
  160. package/dist/plugins/pg-session-storage.js +7 -17
  161. package/dist/plugins/remote-env-adb.d.ts +6 -0
  162. package/dist/plugins/remote-env-adb.js +9 -4
  163. package/dist/plugins/remote-env-device.js +3 -0
  164. package/dist/plugins/remote-env-e2b.d.ts +1 -1
  165. package/dist/plugins/remote-env-e2b.js +21 -9
  166. package/dist/plugins/remote-env-host.d.ts +9 -9
  167. package/dist/plugins/remote-env-host.js +10 -32
  168. package/dist/plugins/remote-env-k8s.d.ts +95 -26
  169. package/dist/plugins/remote-env-k8s.js +265 -90
  170. package/dist/plugins/remote-env-local-docker.d.ts +20 -1
  171. package/dist/plugins/remote-env-local-docker.js +77 -9
  172. package/dist/plugins/remote-env-ssh.d.ts +15 -0
  173. package/dist/plugins/remote-env-ssh.js +109 -3
  174. package/dist/plugins/remote-shell.d.ts +0 -1
  175. package/dist/plugins/remote-shell.js +1 -2
  176. package/dist/plugins/retention-store-sql.js +6 -2
  177. package/dist/plugins/schema-consistency.d.ts +54 -0
  178. package/dist/plugins/schema-consistency.js +52 -0
  179. package/dist/plugins/session-epoch-anchor.d.ts +13 -0
  180. package/dist/plugins/session-epoch-anchor.js +23 -0
  181. package/dist/plugins/shared-memory-store-sql.d.ts +9 -3
  182. package/dist/plugins/shared-memory-store-sql.js +5 -16
  183. package/dist/plugins/sql-driver.d.ts +141 -0
  184. package/dist/plugins/sql-driver.js +115 -1
  185. package/dist/plugins/sql-errors.d.ts +41 -0
  186. package/dist/plugins/sql-errors.js +36 -0
  187. package/dist/plugins/store-backend.js +2 -1
  188. package/dist/plugins/tidb-session-storage.d.ts +7 -11
  189. package/dist/plugins/tidb-session-storage.js +6 -16
  190. package/dist/plugins/tidb-session-store.js +1 -1
  191. package/dist/prompts-domain-validate.d.ts +17 -61
  192. package/dist/prompts-domain-validate.js +9 -119
  193. package/dist/request-key-closure.d.ts +2 -2
  194. package/dist/request-key-closure.js +2 -4
  195. package/dist/rule-entry-list.d.ts +12 -9
  196. package/dist/rule-entry-list.js +37 -21
  197. package/dist/run-local.js +19 -19
  198. package/dist/runs.d.ts +6 -0
  199. package/dist/runs.js +16 -0
  200. package/dist/server-secret-env.d.ts +44 -18
  201. package/dist/server-secret-env.js +49 -8
  202. package/dist/session-incarnation.d.ts +15 -5
  203. package/dist/session-incarnation.js +16 -9
  204. package/dist/session-titler.js +12 -2
  205. package/dist/spec-fields.d.ts +28 -16
  206. package/dist/spec-fields.js +46 -53
  207. package/dist/sql-engine-posture.d.ts +29 -4
  208. package/dist/sql-engine-posture.js +36 -1
  209. package/dist/stored-request-tool-axes-m19.d.ts +19 -4
  210. package/dist/stored-request-tool-axes-m19.js +30 -2
  211. package/dist/task-a2a.d.ts +9 -1
  212. package/dist/task-a2a.js +73 -47
  213. package/dist/task-mcp.d.ts +46 -58
  214. package/dist/task-mcp.js +99 -65
  215. package/dist/task-settings.d.ts +7 -21
  216. package/dist/task-settings.js +7 -36
  217. package/dist/text-bidi.d.ts +3 -46
  218. package/dist/text-bidi.js +3 -3
  219. package/dist/tool-approval.d.ts +1 -1
  220. package/dist/trace/core-keyset-guard.d.ts +24 -4
  221. package/dist/trace/engine-notice-wire.d.ts +11 -3
  222. package/dist/trace/engine-notice-wire.js +8 -1
  223. package/dist/trace/ledger-events.d.ts +3 -1
  224. package/dist/trace/ledger-sink.js +4 -1
  225. package/dist/trace/model-segment.js +1 -1
  226. package/dist/trace/project.d.ts +19 -6
  227. package/dist/trace/project.js +57 -22
  228. package/dist/trace/wire-projection-faces.d.ts +79 -1
  229. package/dist/trace/wire-projection-faces.js +22 -0
  230. package/dist/web-admin-client/candidate-gate.d.ts +6 -6
  231. package/dist/web-admin-client/candidate-gate.js +3 -3
  232. package/dist/web-admin-client/http-client.js +0 -6
  233. package/dist/web-admin-client/read-warnings.d.ts +13 -0
  234. package/dist/web-admin-client/read-warnings.js +10 -0
  235. package/dist/web-admin-client/skills-mcp.d.ts +3 -0
  236. package/dist/web-admin-client/skills-mcp.js +9 -6
  237. package/dist/web-admin-client/types.d.ts +2 -3
  238. package/dist/workspace-cache.d.ts +35 -0
  239. package/dist/workspace-cache.js +36 -0
  240. package/dist/workspace-space.d.ts +3 -2
  241. package/dist/workspace-space.js +2 -0
  242. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +8 -1
  243. package/package.json +4 -4
  244. package/scripts/drain-check-local.mjs +34 -1
  245. package/scripts/drain-check.mysql.sql +30 -2
  246. package/scripts/drain-check.pg.sql +29 -2
  247. package/dist/capabilities/memory-notice.d.ts +0 -138
  248. package/dist/capabilities/memory-notice.js +0 -64
  249. package/dist/capabilities/prompt.d.ts +0 -17
  250. package/dist/capabilities/prompt.js +0 -6
  251. package/dist/mcp-content-origin.d.ts +0 -34
  252. package/dist/mcp-content-origin.js +0 -10
  253. package/dist/tool-axis-words.d.ts +0 -27
  254. package/dist/tool-axis-words.js +0 -9
  255. package/dist/wire-name-bound.d.ts +0 -14
  256. package/dist/wire-name-bound.js +0 -13
@@ -1,15 +1,18 @@
1
- import { type Hooks } from "@sema-agent/core";
1
+ import { type EngineDollar, type ModRegistration } from "@sema-agent/core";
2
2
  import type { Logger } from "./logger.js";
3
3
  import type { Metrics } from "./metrics.js";
4
+ /** 两张部署注册的名字 —— wire 可见(`wiring_manifest.mods[].name`);名字中性、稳定、不含部署信息。 */
5
+ export declare const PERMISSION_DENIED_METER_REGISTRATION = "server.permission-denied-meter";
6
+ export declare const TOOL_TRACE_REGISTRATION = "server.tool-trace";
4
7
  /**
5
8
  * ALWAYS-ON gate-deny meter (goal B4, 2026-07-08): `permission_denied_total{deniedBy}` — how often the
6
9
  * adjudicate chain DENIES a tool call, by the LAYER whose verdict is the deny. Deliberately SEPARATE from
7
10
  * {@link createToolTracer}: the tracer is a default-OFF diagnostic (TOOL_TRACE=true), but deny frequency is an
8
11
  * operational/security signal that must reach /metrics on every deployment — a spike in `deniedBy="policy"`
9
12
  * means a task keeps asking for something the operator forbids; `deniedBy="org"` is the organization layer
10
- * refusing, the one class an operator most wants to see on its own. main.ts composes this UNDER the tracer via
11
- * composeHooks (both are void observers on the permissionDenied slot, which composeHooks double-runs
12
- * deployment-first).
13
+ * refusing, the one class an operator most wants to see on its own. It is the deployment seat's first registration
14
+ * (`createDeploymentMods`, `boot/deployment-mods.ts`): an observer on `classic.PermissionDenied`, run before the tracer's observer on the same
15
+ * event (the engine runs observers by tier, then deployment before task, then each seat's own order).
13
16
  *
14
17
  * 🔴 BREAKING(server 7.64.0,core 7.6.0 提货):标签名 `source` → `deniedBy`,词表从七词
15
18
  * (`policy/hook/safety/shellGate/planMode/classifier/org`)换成八词
@@ -46,7 +49,7 @@ import type { Metrics } from "./metrics.js";
46
49
  * · **计数总量方向**:新座只在「分类器那一轮没有裁决」时在场(`<block>yes` 的真裁决恒缺席),与旧座的
47
50
  * 「分类器停摆」同向;多出来的那一格是 `parse_error`,它也确实是「分类器没正常工作」的一种。
48
51
  */
49
- export declare function createPermissionDeniedMeter(metrics: Pick<Metrics, "inc">): Hooks;
52
+ export declare function createPermissionDeniedMeter(metrics: Pick<Metrics, "inc">): ModRegistration<EngineDollar>;
50
53
  /**
51
54
  * prove-before-scale stopgap ([ref]): a no-op observer hook that logs every tool call (pre + post)
52
55
  * to the worker log — INCLUDING resumed legs that `GET /v1/runs/:id/events` does NOT surface (the
@@ -59,21 +62,13 @@ export declare function createPermissionDeniedMeter(metrics: Pick<Metrics, "inc"
59
62
  * the full sequence: a repeated `write_file path=X contentLen=Y` across resumes ⇒ restart-loop;
60
63
  * write X → write Y → bash ⇒ iteration.
61
64
  *
62
- * Pure observation: every hook returns `undefined`, so it never rewrites args, never gates, never alters
63
- * the result (and per core's hooks invariant a hook's `allow` can't bypass the policy gate anyway).
64
- *
65
- * 🔴 core 5.20.0 起这条「纯观察」性质必须**声明**出来(`preToolUseObservational`),不能只写在注释里:
66
- * core 5.19.0 的委派折叠按「PreToolUse 席位在不在」把父的 screening 面折进子代的祖先约束链——它分不出
67
- * 诊断 tracer 与真裁决者。后果是**这个默认关的诊断旋钮**一开,该部署所有 hook-wired 父的 parked 子代
68
- * checkpoint 就记 `parentConstraintCount=2`,而跨副本赎回腿(`boot/parked-revive-gate.ts`)恒供 1 ⇒
69
- * core pre-CAS 响亮拒、行永久 pending(层数 park 时写死,事后关旋钮/重批/回滚都不改)。
70
- * 声明之后该面不铸链条目,count 回到 1,病路消灭。
71
- * ⛔ 打这个标的前提是**真的一条裁决都不出**:core 对声明了却返回判词的面是「拒绝采纳 + onError 告警」
72
- * (classification `observational-hook-verdict-ignored`),即判词被静默丢弃——把标打到会裁决的面上比原
73
- * 缺陷更坏。本文件三个 pre/post 回调恒返 `undefined`,`permissionDenied` 本就是 void 观察槽。
74
- * 📎 连带的**可观测**变化(不是回归):core 的「可写工具面无 effect-aware 门」启动告警此前把本 tracer
75
- * 算作一道门而被抑制;声明之后它不再算,于是「没配任何 policy 层 + 开着 TOOL_TRACE」的部署升级后会
76
- * 多出那行告警。方向正确——tracer 从来没有门住任何东西,原先的抑制本身就是一次静默 fail-open。
65
+ * Pure observation: every handler is an observer (`{ observe: true }`, via {@link observeClassic}) — a detached copy of
66
+ * the event, outside the chain, its answer never adopted — so it never rewrites args, never gates, never alters the result.
67
+ * Being an observer is also what keeps it out of a delegated child's inherited constraint: the engine mints a screening
68
+ * entry only from a chain that JUDGES, so turning this diagnostic switch on adds no `parentConstraintCount` to a parked
69
+ * child of a hook-wired parent (the cross-replica revive leg, `boot/parked-revive-gate.ts`, supplies the caller policy only).
70
+ * 📎 Not a gate either for the engine's "writable tool face without an effect-aware gate" startup warning: a deployment with
71
+ * no policy layer and `TOOL_TRACE` on still gets that warning — the tracer never gated anything.
77
72
  */
78
- export declare function createToolTracer(logger: Logger): Hooks;
73
+ export declare function createToolTracer(logger: Logger): ModRegistration<EngineDollar>;
79
74
  //# sourceMappingURL=tool-trace.d.ts.map
@@ -1,60 +1,69 @@
1
1
  import { isClassifierDenyCause, isDeniedBy } from "@sema-agent/core";
2
+ import { observeClassic } from "../hooks/classic-stations.js";
2
3
  import { redactSecrets } from "../trace/redact.js";
4
+ export const PERMISSION_DENIED_METER_REGISTRATION = "server.permission-denied-meter";
5
+ export const TOOL_TRACE_REGISTRATION = "server.tool-trace";
3
6
  function deniedByLabel(gate) {
4
7
  const d = gate.disposition;
5
8
  return d.kind === "denied" && isDeniedBy(d.deniedBy) ? d.deniedBy : "other";
6
9
  }
7
10
  export function createPermissionDeniedMeter(metrics) {
8
11
  return {
9
- permissionDenied(payload) {
10
- metrics.inc("permission_denied_total", { deniedBy: deniedByLabel(payload.gate) });
11
- const disposition = payload.gate?.disposition;
12
- const cause = disposition?.kind === "denied" ? disposition.cause : undefined;
13
- if (cause !== undefined) {
14
- metrics.inc("auto_mode_classifier_deny_cause_total", { cause: isClassifierDenyCause(cause) ? cause : "other" });
15
- }
12
+ name: PERMISSION_DENIED_METER_REGISTRATION,
13
+ tier: "prepend",
14
+ uses: { events: ["classic.PermissionDenied"], calls: [] },
15
+ register(on) {
16
+ observeClassic(on, "classic.PermissionDenied", (payload) => {
17
+ metrics.inc("permission_denied_total", { deniedBy: deniedByLabel(payload.gate) });
18
+ const disposition = payload.gate?.disposition;
19
+ const cause = disposition?.kind === "denied" ? disposition.cause : undefined;
20
+ if (cause !== undefined) {
21
+ metrics.inc("auto_mode_classifier_deny_cause_total", { cause: isClassifierDenyCause(cause) ? cause : "other" });
22
+ }
23
+ });
16
24
  },
17
25
  };
18
26
  }
19
27
  export function createToolTracer(logger) {
20
28
  return {
21
- preToolUseObservational: true,
22
- preToolUse(toolName, input, ctx) {
23
- logger.info("tool_trace", { phase: "pre", id: ctx.toolCallId, tool: toolName, arg: summarize(input) });
24
- return undefined;
25
- },
26
- postToolUse(toolName, _input, output, ctx) {
27
- logger.info("tool_trace", {
28
- phase: "post",
29
- id: ctx.toolCallId,
30
- tool: toolName,
31
- isError: output.isError,
32
- out: textOf(output.content),
29
+ name: TOOL_TRACE_REGISTRATION,
30
+ tier: "prepend",
31
+ uses: { events: ["classic.PreToolUse", "classic.PostToolUse", "classic.PostToolUseFailure", "classic.PermissionDenied"], calls: [] },
32
+ register(on) {
33
+ observeClassic(on, "classic.PreToolUse", (e) => {
34
+ logger.info("tool_trace", { phase: "pre", id: e.tool_use_id, tool: e.tool, arg: summarize(e.input) });
33
35
  });
34
- return undefined;
35
- },
36
- postToolUseFailure(toolName, _input, failure, ctx) {
37
- logger.info("tool_trace", {
38
- phase: "post",
39
- id: ctx.toolCallId,
40
- tool: toolName,
41
- isError: true,
42
- isInterrupt: failure.isInterrupt,
43
- ...(failure.durationMs !== undefined ? { durationMs: failure.durationMs } : {}),
44
- out: clip(redactSecrets(failure.error), 200),
36
+ observeClassic(on, "classic.PostToolUse", (e) => {
37
+ logger.info("tool_trace", {
38
+ phase: "post",
39
+ id: e.tool_use_id,
40
+ tool: e.tool,
41
+ isError: e.output.isError,
42
+ out: textOf(e.output.content),
43
+ });
45
44
  });
46
- return undefined;
47
- },
48
- permissionDenied(payload) {
49
- logger.info("tool_trace", {
50
- phase: "denied",
51
- id: payload.toolCallId,
52
- tool: payload.toolName,
53
- deniedBy: deniedByLabel(payload.gate),
54
- ...(payload.gate.origin === undefined ? {} : { origin: payload.gate.origin }),
55
- ...(payload.gate.settlement === undefined ? {} : { settlement: payload.gate.settlement.kind }),
56
- reason: clip(redactSecrets(payload.reason), 200),
57
- arg: summarize(payload.input),
45
+ observeClassic(on, "classic.PostToolUseFailure", (e) => {
46
+ logger.info("tool_trace", {
47
+ phase: "post",
48
+ id: e.tool_use_id,
49
+ tool: e.tool,
50
+ isError: true,
51
+ isInterrupt: e.failure.isInterrupt,
52
+ ...(e.failure.durationMs !== undefined ? { durationMs: e.failure.durationMs } : {}),
53
+ out: clip(redactSecrets(e.failure.error), 200),
54
+ });
55
+ });
56
+ observeClassic(on, "classic.PermissionDenied", (payload) => {
57
+ logger.info("tool_trace", {
58
+ phase: "denied",
59
+ id: payload.toolCallId,
60
+ tool: payload.toolName,
61
+ deniedBy: deniedByLabel(payload.gate),
62
+ ...(payload.gate.origin === undefined ? {} : { origin: payload.gate.origin }),
63
+ ...(payload.gate.settlement === undefined ? {} : { settlement: payload.gate.settlement.kind }),
64
+ reason: clip(redactSecrets(payload.reason), 200),
65
+ arg: summarize(payload.input),
66
+ });
58
67
  });
59
68
  },
60
69
  };
@@ -4,13 +4,19 @@ import type { ImageViewer, ImageIndexEntry } from "./plugins/store-contracts.js"
4
4
  export interface SandboxImageResolver {
5
5
  latestPublished(profile: string, viewer: ImageViewer): Promise<Pick<ImageIndexEntry, "repo" | "digest" | "capabilities"> | null>;
6
6
  }
7
+ /**
8
+ * 解析失败的机读码(闭集):`not_found.image` = 按调用方的约束(档案 · 本主体可见 · 已发布 · 所需能力)找不到镜像 —— 与 `POST /v1/images/select`
9
+ * 「没有可见的已发布镜像」同一个码(同一个事实);`request.field_invalid` = `capabilitiesNeeded` 里有不认识的能力词。状态随码,一码一状态。
10
+ */
11
+ export type SandboxImageRefusalCode = "not_found.image" | "request.field_invalid";
7
12
  export type SandboxImageResolution = {
8
13
  ok: true;
9
14
  ref: string;
10
15
  capabilities?: import("./plugins/store-contracts.js").ImageCapabilities;
11
16
  } | {
12
17
  ok: false;
13
- status: number;
18
+ status: 404 | 400;
19
+ errorCode: SandboxImageRefusalCode;
14
20
  message: string;
15
21
  };
16
22
  /**
@@ -3,15 +3,15 @@ const REQUESTABLE_CAPS = new Set(["browser", "db", "nestedBuild"]);
3
3
  export async function resolveSandboxImageRef(opts) {
4
4
  const entry = await opts.index.latestPublished(opts.profile, opts.viewer);
5
5
  if (!entry) {
6
- return { ok: false, status: 404, message: `no published sandbox image for profile "${opts.profile}" visible to this principal` };
6
+ return { ok: false, status: 404, errorCode: "not_found.image", message: `no published sandbox image for profile "${opts.profile}" visible to this principal` };
7
7
  }
8
8
  const caps = entry.capabilities;
9
9
  for (const cap of opts.capabilitiesNeeded ?? []) {
10
10
  if (!REQUESTABLE_CAPS.has(cap)) {
11
- return { ok: false, status: 400, message: `unknown sandbox capability "${cap}" (allowed: ${[...REQUESTABLE_CAPS].join(", ")})` };
11
+ return { ok: false, status: 400, errorCode: "request.field_invalid", message: `unknown sandbox capability "${cap}" (allowed: ${[...REQUESTABLE_CAPS].join(", ")})` };
12
12
  }
13
13
  if (caps[cap] !== true) {
14
- return { ok: false, status: 404, message: `sandbox image for profile "${opts.profile}" does not provide capability "${cap}"` };
14
+ return { ok: false, status: 404, errorCode: "not_found.image", message: `sandbox image for profile "${opts.profile}" does not provide capability "${cap}"` };
15
15
  }
16
16
  }
17
17
  return { ok: true, ref: `${entry.repo}@${entry.digest}`, capabilities: entry.capabilities };
@@ -0,0 +1,25 @@
1
+ /** 一个没进副本的成员:名字(调用方写的,原样;回显前由调用点清洗)与它为什么不能交(只说种类,不回显值)。 */
2
+ export interface PlainStringRecordMemberFault {
3
+ readonly name: string;
4
+ readonly fault: string;
5
+ }
6
+ /**
7
+ * {@link readPlainStringRecord} 的读数(见模块头注的律):`ok:false` = 记录不交(`fault` 只说种类;坏在某个成员时 `member` 点名它);
8
+ * `ok:true` = 交出去的纯数据副本 + 被剔掉的 `__proto__` 名成员(按记录里的顺序;恒只有这一种成员会被剔)。
9
+ * `ok:false` 的 `fault` 是接在「its member "<名>" 」后面的原因句;成员名恰是 `__proto__` 而值也坏时,这一句同时说两件事(名不许、值必须是串)。
10
+ */
11
+ export type PlainStringRecordRead = {
12
+ readonly ok: true;
13
+ readonly record: Record<string, string>;
14
+ readonly stripped: readonly PlainStringRecordMemberFault[];
15
+ } | {
16
+ readonly ok: false;
17
+ readonly fault: string;
18
+ readonly member?: string;
19
+ };
20
+ /**
21
+ * 读一份「名 → 串」记录(见模块头注的律)。成员 = `Object.getOwnPropertyNames` 的每一个名字(不可枚举的也算,与引擎收据同读法),值按属性描述读:
22
+ * 数据属性取值,访问器调一次。先判每个成员的值(`__proto__` 名的成员也判:它的值不是串 ⇒ 整只不交,与 7.131.x 同),再剔 `__proto__` 名。调用方的对象不被交出去、也不被改。
23
+ */
24
+ export declare function readPlainStringRecord(v: unknown): PlainStringRecordRead;
25
+ //# sourceMappingURL=plain-string-record.d.ts.map
@@ -0,0 +1,32 @@
1
+ import { describeValueShape } from "@sema-agent/settings-schema";
2
+ const PROTO_MEMBER_FAULT = 'no member of a declaration may be named "__proto__"';
3
+ export function readPlainStringRecord(v) {
4
+ if (v === null || typeof v !== "object" || Array.isArray(v))
5
+ return { ok: false, fault: v === null ? "got null" : Array.isArray(v) ? "got an array" : `got a ${typeof v}` };
6
+ const proto = Object.getPrototypeOf(v);
7
+ if (proto !== Object.prototype && proto !== null)
8
+ return { ok: false, fault: "got an object that is not plain data (a class instance, a Map, process.env, or a record built on a prototype; copy it into a plain object)" };
9
+ const record = {};
10
+ const stripped = [];
11
+ for (const name of Object.getOwnPropertyNames(v)) {
12
+ const d = Object.getOwnPropertyDescriptor(v, name);
13
+ let value;
14
+ let unreadable = false;
15
+ try {
16
+ value = d === undefined ? undefined : Object.hasOwn(d, "value") ? d.value : d.get === undefined ? undefined : Reflect.apply(d.get, v, []);
17
+ }
18
+ catch {
19
+ unreadable = true;
20
+ }
21
+ if (unreadable || typeof value !== "string") {
22
+ const valueFault = unreadable ? "could not be read (its getter threw)" : `must be a string — got ${describeValueShape(value)}`;
23
+ return { ok: false, fault: name === "__proto__" ? `is not allowed (${PROTO_MEMBER_FAULT}) and its value ${valueFault}` : valueFault, member: name };
24
+ }
25
+ if (name === "__proto__")
26
+ stripped.push({ name, fault: PROTO_MEMBER_FAULT });
27
+ else
28
+ record[name] = value;
29
+ }
30
+ return { ok: true, record, stripped };
31
+ }
32
+ //# sourceMappingURL=plain-string-record.js.map
@@ -1,19 +1,69 @@
1
1
  import type { Pool as MysqlPool } from "mysql2/promise";
2
2
  import type { Pool as PgPool } from "pg";
3
- /** (a)(clay 拍 a+c,2026-07-27)tidb 方言的默认 per-blob 帽 = **6 MiB,TiDB 自己的默认
4
- * `txn-entry-size-limit`**。两堵墙真库实测(k3s8 tidb-test v7.5.1 默认配置,2026-07-27 探针):
5
- * 7MiB=`entry too large`(entry 墙,默认 6291456)、31MiB=同、32MiB=`max_allowed_packet`(包墙,
6
- * 64MiB ÷ ~2× 文本协议膨胀)——矮墙是 entry 的 6MiB,不是先前推测的包墙 ~31MiB。帽=诚实拒绝:
7
- * 超限 blob 在 query 之前 typed 拒(消费面 HTTP 413 / capture warn 拿到可行动信息);**帽拒绝的
8
- * 在默认配置下本来也落不进去 ⇒ 零回归**。调大过 entry 限/packet 的部署用 SNAPSHOT_BLOB_SQL_MAX_BYTES
9
- * 放宽(两方言都生效)。**pg 方言默认不设帽**(扩展协议无这两堵墙,不人为降档)。D-1 附件战役同墙
10
- * 教训:task-attachment-store.ts:19。 */
11
- export declare const SQL_BLOB_DEFAULT_MAX_BYTES = 6291456;
3
+ import { type SqlEngineReading, type SqlWallSource, type SqlWallsReading } from "./sql-driver.js";
4
+ /**
5
+ * 文本协议的 blob 膨胀律 —— mysql2 `query()` 把 Buffer 参数写成 `X'<hex>'`,一字节两个字符,所以承载一个 blob 的语句至少是它的 2 倍。
6
+ * 包墙折成「能装下多大的 blob」只用这一只常量,推导里不手写 `/ 2`。
7
+ */
8
+ export declare const SQL_TEXT_PROTOCOL_BLOB_INFLATION = 2;
9
+ /**
10
+ * 包墙下能装多大的 blob:语句里的载荷量({@link packetWallStatementBytes},与提交体受理口读同一个折算)再按 {@link SQL_TEXT_PROTOCOL_BLOB_INFLATION} 折半。
11
+ * 提交体按原样进语句、没有膨胀,blob 以十六进制字面进语句、有 2× —— 膨胀律是 blob 腿自己的,余量不是。
12
+ */
13
+ export declare function packetWallBlobCap(maxPacketBytes: number): number;
14
+ /**
15
+ * **事实本身还没建立**时 MySQL 协议方言的缺省帽(池上还没借出过连接,或最近一次连接初始化被拒:`SqlDriver.facts === null`)= 事实缺席时的墙
16
+ * (`sql-driver.ts` `SQL_FACTS_ABSENT_WALLS`,判官 `sqlWallsOf`;提交受理口读同一只)走下面同一个推导 —— 不是第二份落点,只是那个推导在这组墙上的值
17
+ * (今天 = TiDB 缺省行墙走 blob 行的折算 6 MiB − 1 KiB:最矮的包墙缺省 16 MiB 扣语句余量再折半仍近 8 MiB,行腿更矮)。
18
+ */
19
+ export declare const SQL_BLOB_FACTS_ABSENT_CAP_BYTES: number;
20
+ /**
21
+ * SQL 快照 blob 的**缺省**帽(运维没设 `SNAPSHOT_BLOB_SQL_MAX_BYTES` 时)的唯一推导 —— 入参是判官 `sqlWallsOf` 给的墙(不读事实、不判缺席、不看方言),**两堵墙取最小**:
22
+ * 行墙腿(`SqlEngineFacts.maxRowBytes`,TiDB:{@link rowWallPayloadBytes} 的 `blob` = 行墙 − blob 行的余量 1 KiB —— 按这一行的真实形算,不借 JSON 提交体行的 64 KiB)
23
+ * 与包墙腿(`SqlEngineFacts.maxPacketBytes`,InnoDB 与 TiDB 都有:{@link packetWallBlobCap} = ⌊(包墙 − 1 KiB 语句余量) ÷ 2⌋)。余量只在 `sql-driver.ts` 定义一次
24
+ * (墙 × 行的形各一只),提交体受理口(`http/request-guards.ts` `refuseSubmitBody`)读同一张表的 `json` 那一只,不各抄。
25
+ * 墙的 `null` 只表示「这堵墙不存在」;两堵都不存在(PG —— 扩展协议无这两堵墙,事实在不在都一样 —— 或没有 SQL 后端)⇒ `undefined` 不设帽。事实本身缺席时
26
+ * 墙由判官给(MySQL 协议按最保守的墙),走同一个推导 = {@link SQL_BLOB_FACTS_ABSENT_CAP_BYTES}。显式旋钮是运维的策略帽,在本推导之上(见 {@link sqlBlobCapBytes})。
27
+ *
28
+ * 两堵墙的真库读数(clay 拍 a+c,2026-07-27;k3s8 tidb-test v7.5.1 默认配置探针):7 MiB / 31 MiB = `entry too large`(行墙,默认 6291456)、
29
+ * 32 MiB = `max_allowed_packet`(包墙,64 MiB ÷ ~2× 文本协议膨胀)。帽=诚实拒绝:超限 blob 在 query 之前 typed 拒(消费面 HTTP 413 / capture warn
30
+ * 拿到可行动信息);帽与墙之间只差这一行自己的几百字节(blob 行余量),帽之下的 blob 在缺省配置下落得进。D-1 附件战役同墙教训:`task-attachment-store.ts` 头注。
31
+ */
32
+ export declare function sqlBlobDefaultCapBytes(walls: SqlWallsReading): number | undefined;
33
+ /**
34
+ * 一个 SQL 快照 blob 的帽 + 它**从哪来**。`operator` = 运维显式设的 `SNAPSHOT_BLOB_SQL_MAX_BYTES`(策略帽);`reported` / `assumed` = 缺省推导里**定下这个帽的那条腿**的墙数字出处
35
+ * ({@link SqlWallSource}:库自己报的 / 读失败或事实缺席而退的文档缺省)。机读位同 413 拒体的 `wallSource` 词,旋钮形多一个 `operator`。
36
+ */
37
+ export interface SqlBlobCapReading {
38
+ readonly capBytes: number;
39
+ readonly source: "operator" | SqlWallSource;
40
+ }
41
+ /**
42
+ * 缺省帽的推导(唯一一处,{@link sqlBlobDefaultCapBytes} 是它的字节投影):两条腿各带自己那堵墙的出处,取最小的那条;两条一样小取行墙腿。两堵墙都不存在 ⇒ `undefined`。
43
+ */
44
+ export declare function sqlBlobDefaultCapOf(walls: SqlWallsReading): SqlBlobCapReading | undefined;
45
+ /**
46
+ * 一个 SQL 快照 blob 此刻的帽 —— **显式旋钮 ?? 判官给的墙走缺省推导**的唯一一只函数。两个读者同调它、各传本池的**引擎读口**(方言与活事实同出一只驱动,
47
+ * 与提交受理口读的是同一种读口):店的写前帽({@link SqlBlobBackend},本池的驱动)与 `PUT /sync/blobs` 的前置 413(`boot/stage-10-http-server.ts` 装配的读口,
48
+ * 按请求调,读 `ServiceDeps.sqlEngine`)。同一池上两个读口读的是同一份池级事实。
49
+ */
50
+ export declare function sqlBlobCapOf(explicitCapBytes: number | undefined, engine: SqlEngineReading): SqlBlobCapReading | undefined;
51
+ /** {@link sqlBlobCapOf} 的字节投影(同一个推导,不另读)。 */
52
+ export declare function sqlBlobCapBytes(explicitCapBytes: number | undefined, engine: SqlEngineReading): number | undefined;
53
+ /**
54
+ * 一次 blob 超帽的拒句 —— **唯一的组句器**(`PUT /sync/blobs` 的前置 413 与店的 typed {@link BlobTooLargeError} 同调它):句子按帽的出处({@link SqlBlobCapReading.source})
55
+ * 分三形,不把假定说成库的上限、也不把运维的旋钮说成库的上限。
56
+ * · `operator` —— 帽是运维设的 `SNAPSHOT_BLOB_SQL_MAX_BYTES`;
57
+ * · `reported` —— 帽由库报的两堵墙推出(单行上限与包墙÷文本协议膨胀的较小者);
58
+ * · `assumed` —— 库的上限没读到(事实还没建立,或这一堵读失败),帽按受支持引擎里最保守的缺省墙推出,稍后重试或由运维设旋钮。
59
+ */
60
+ export declare function sqlBlobCapRefusalText(byteLen: number, cap: SqlBlobCapReading): string;
12
61
  /** typed 判别错:blob 超过 SQL 店的承载帽。判别码供店侧结果映射/HTTP 面/日志使用——绝不能与
13
- * transient 写失败混在一类(那类是可重试的 502;这类重试永远不会成功)。 */
62
+ * transient 写失败混在一类(那类是可重试的 502;这类重试永远不会成功)。句子是 {@link sqlBlobCapRefusalText}(带帽的出处)。 */
14
63
  export declare class BlobTooLargeError extends Error {
64
+ readonly cap: SqlBlobCapReading;
15
65
  readonly code: "blob_too_large_for_sql";
16
- constructor(byteLen: number, capBytes: number);
66
+ constructor(byteLen: number, cap: SqlBlobCapReading);
17
67
  }
18
68
  /**
19
69
  * The content-addressed byte store. All four methods are keyed on the sha256 `hash` of the bytes:
@@ -47,11 +97,16 @@ export interface BlobBackend {
47
97
  export declare class SqlBlobBackend implements BlobBackend {
48
98
  private readonly dialect;
49
99
  private readonly pool;
50
- /** Effective per-blob cap: explicit ctor value wins; else tidb=SQL_BLOB_DEFAULT_MAX_BYTES, pg=uncapped. */
51
- private readonly capBytes;
100
+ /** Explicit per-blob cap (the operator's `SNAPSHOT_BLOB_SQL_MAX_BYTES`); absent ⇒ the derivation in {@link sqlBlobCapBytes} over this pool's live engine facts. */
101
+ private readonly explicitCapBytes;
102
+ /** This pool's engine reading (dialect + live facts, from the pool's own driver) — the one source the cap's walls are read from. */
103
+ private readonly engine;
52
104
  /** Blob-domain table ([ref] blob 域隔离:file-history 域用自己的表,reap/GC 不跨域)。代码内字面量,非用户输入。 */
53
105
  private readonly table;
54
106
  constructor(dialect: "tidb" | "pg", pool: MysqlPool | PgPool, maxBytes?: number, table?: string);
107
+ /** The cap in force for this write: the explicit one, else the default derived from the pool's engine facts as they
108
+ * stand now (the driver refreshes them on each connection initialization). */
109
+ private cap;
55
110
  /** The created_at-refreshing content-addressed upsert — VERBATIM the tidb/pg stores' `putBlobRow`. Refreshes
56
111
  * created_at even on a dedup hit so a re-used/just-written blob is "touched=now" and GC-grace-protected.
57
112
  * (a): an over-cap blob is REJECTED (typed {@link BlobTooLargeError}) BEFORE the driver ever sees the bytes —
@@ -1,14 +1,51 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { presignS3Url, presignS3ListUrl } from "./s3-presign.js";
3
+ import { SQL_FACTS_ABSENT_WALLS, mysqlDriver, packetWallStatementBytes, pgDriver, rowWallPayloadBytes, sqlWallsOf } from "./sql-driver.js";
3
4
  const BLOB_GC_GRACE_MS = 3_600_000;
4
- export const SQL_BLOB_DEFAULT_MAX_BYTES = 6_291_456;
5
+ export const SQL_TEXT_PROTOCOL_BLOB_INFLATION = 2;
6
+ export function packetWallBlobCap(maxPacketBytes) {
7
+ return Math.floor(packetWallStatementBytes(maxPacketBytes) / SQL_TEXT_PROTOCOL_BLOB_INFLATION);
8
+ }
9
+ export const SQL_BLOB_FACTS_ABSENT_CAP_BYTES = sqlBlobDefaultCapBytes(SQL_FACTS_ABSENT_WALLS);
10
+ export function sqlBlobDefaultCapBytes(walls) {
11
+ return sqlBlobDefaultCapOf(walls)?.capBytes;
12
+ }
13
+ export function sqlBlobDefaultCapOf(walls) {
14
+ const legs = [];
15
+ if (walls.maxRowBytes !== null)
16
+ legs.push({ capBytes: rowWallPayloadBytes(walls.maxRowBytes, "blob"), source: walls.sources.maxRowBytes });
17
+ if (walls.maxPacketBytes !== null)
18
+ legs.push({ capBytes: packetWallBlobCap(walls.maxPacketBytes), source: walls.sources.maxPacketBytes });
19
+ return legs.reduce((best, leg) => (best === undefined || leg.capBytes < best.capBytes ? leg : best), undefined);
20
+ }
21
+ export function sqlBlobCapOf(explicitCapBytes, engine) {
22
+ return explicitCapBytes !== undefined ? { capBytes: explicitCapBytes, source: "operator" } : sqlBlobDefaultCapOf(sqlWallsOf(engine));
23
+ }
24
+ export function sqlBlobCapBytes(explicitCapBytes, engine) {
25
+ return sqlBlobCapOf(explicitCapBytes, engine)?.capBytes;
26
+ }
27
+ export function sqlBlobCapRefusalText(byteLen, cap) {
28
+ const objectStorage = "configure MinIO object storage (MINIO_ENDPOINT/MINIO_ACCESS_KEY/MINIO_SECRET_KEY) for large snapshot blobs";
29
+ const head = `blob of ${byteLen} bytes exceeds this deployment's SQL snapshot-blob cap (${cap.capBytes} bytes — `;
30
+ switch (cap.source) {
31
+ case "operator":
32
+ return `${head}set by the operator's SNAPSHOT_BLOB_SQL_MAX_BYTES) — ${objectStorage}, or raise SNAPSHOT_BLOB_SQL_MAX_BYTES if the database allows larger writes`;
33
+ case "reported":
34
+ return `${head}the smaller of the database's single-row limit (TiDB txn-entry-size-limit, 6MiB by default) and about half its max_allowed_packet (the text protocol doubles a blob inside the statement)) — ${objectStorage}, or raise SNAPSHOT_BLOB_SQL_MAX_BYTES if your deployment lifted those limits`;
35
+ case "assumed":
36
+ return `${head}the database's limits were not read — engine facts not read yet, or a limit could not be read — so the smallest documented default wall applies) — retry shortly, ${objectStorage}, or set SNAPSHOT_BLOB_SQL_MAX_BYTES to the limit your deployment actually has`;
37
+ default: {
38
+ const unhandled = cap.source;
39
+ throw new Error(`blob cap refusal without a sentence: ${String(unhandled)}`);
40
+ }
41
+ }
42
+ }
5
43
  export class BlobTooLargeError extends Error {
44
+ cap;
6
45
  code = "blob_too_large_for_sql";
7
- constructor(byteLen, capBytes) {
8
- super(`blob of ${byteLen} bytes exceeds the SQL snapshot-blob cap (${capBytes} bytes): a single-row INSERT this large ` +
9
- `hits TiDB's txn-entry-size-limit (default 6MiB) or the mysql-protocol max_allowed_packet (~2x text-protocol ` +
10
- `inflation). Configure object storage (MINIO_ENDPOINT/MINIO_ACCESS_KEY/MINIO_SECRET_KEY) for large snapshot ` +
11
- `blobs, or override the cap with SNAPSHOT_BLOB_SQL_MAX_BYTES if your deployment raised those limits.`);
46
+ constructor(byteLen, cap) {
47
+ super(sqlBlobCapRefusalText(byteLen, cap));
48
+ this.cap = cap;
12
49
  this.name = "BlobTooLargeError";
13
50
  }
14
51
  }
@@ -26,17 +63,23 @@ async function mapBounded(items, limit, fn) {
26
63
  export class SqlBlobBackend {
27
64
  dialect;
28
65
  pool;
29
- capBytes;
66
+ explicitCapBytes;
67
+ engine;
30
68
  table;
31
69
  constructor(dialect, pool, maxBytes, table = "snapshot_blob") {
32
70
  this.dialect = dialect;
33
71
  this.pool = pool;
34
- this.capBytes = maxBytes ?? (dialect === "tidb" ? SQL_BLOB_DEFAULT_MAX_BYTES : Number.POSITIVE_INFINITY);
72
+ this.explicitCapBytes = maxBytes;
35
73
  this.table = table;
74
+ this.engine = dialect === "tidb" ? mysqlDriver(pool) : pgDriver(pool);
75
+ }
76
+ cap() {
77
+ return sqlBlobCapOf(this.explicitCapBytes, this.engine);
36
78
  }
37
79
  async putBlob(hash, bytes) {
38
- if (bytes.byteLength > this.capBytes)
39
- throw new BlobTooLargeError(bytes.byteLength, this.capBytes);
80
+ const cap = this.cap();
81
+ if (cap !== undefined && bytes.byteLength > cap.capBytes)
82
+ throw new BlobTooLargeError(bytes.byteLength, cap);
40
83
  if (this.dialect === "tidb") {
41
84
  await this.pool.query(`INSERT INTO ${this.table} (blob_hash, byte_len, bytes, created_at) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE created_at = VALUES(created_at)`, [hash, bytes.byteLength, Buffer.from(bytes), new Date()]);
42
85
  }
@@ -51,11 +51,12 @@ export interface PendingCheckpoint extends ParkRowFacts {
51
51
  * consumer to treat the key's PRESENCE as "there is a binding" and echo `null` back into /decide. */
52
52
  boundCallId?: string;
53
53
  boundInputHash?: string;
54
- /** 🔴 [ref]③ (additive): the checkpoint's gate kind — the operator queue mixes tool approvals with
54
+ /** 🔴 [ref]③: the checkpoint's gate kind — the operator queue mixes tool approvals with
55
55
  * plan_review / resource_limit / task_done parks (all of which carry `toolName: null`), so a durable-recovery
56
- * consumer needs this to ROUTE a row to the right wire (`/decide` vs `/plan_review` vs `/resume`). Absent
57
- * only for a legacy SQL row suspended before the `gate_kind` column existed. */
58
- gateKind?: CheckpointGate["kind"];
56
+ * consumer needs this to ROUTE a row to the right wire (`/decide` vs `/plan_review` vs `/resume`). ALWAYS present
57
+ * (S-1681, 7.132.0): every park stamps it, the one-time migration M21 filled any row without it from the row's gate,
58
+ * and a SQL row that still carries none is not listed by `listPending` (counted: `server.approvals.queue-row-gate-kind-missing`; no decide route binds it either). */
59
+ gateKind: CheckpointGate["kind"];
59
60
  /** The suspended run's taskId (task_active join) — the key to the run/trace ("view task context"). */
60
61
  taskId: string | null;
61
62
  /** The pending tool call's args (post-hook), redacted + bounded — for tool_approval the write payload, for a
@@ -278,6 +279,15 @@ export declare function assertCheckpointColumnSchema(query: (sql: string) => Pro
278
279
  export declare function boundedRuleOffers(raw: unknown, opts?: {
279
280
  countRedactions?: boolean;
280
281
  }): RuleOfferProjection[] | undefined;
282
+ /**
283
+ * S-1681 —— 运维队列行的门型:**两只孪生同一只判官**(SQL 孪生读 `gate_kind` 列 —— put 时从同一只门反范式下来的那一格,每次停驻都盖,一次性迁移 M21 把没有它的
284
+ * 存量行从门子树补齐;local 孪生读快照上的门 `cp.gate.kind`)。
285
+ * 读到的值是 NULL / 缺席 / 不是 core 登记的门型 ⇒ `undefined`:这一行**不列**(响亮:计一次 `server.approvals.queue-row-gate-kind-missing`)—— 与每一条决断口同一个读法:
286
+ * 按门型绑定取行的读口对这种行本来就不命中(`pendingRowQuery` 的 `gate_kind IN (…)`,证不出门型就不当它是这道门),队列不再把一行谁也决不了的行投出去,
287
+ * 也不按缺键投(消费方按 `gateKind` 路由、缺键即 fail-closed)。不整只拒:一行坏行不许让整个租户的队列(列表 500、SSE 心跳照常而队列永远停在旧帧)消失。
288
+ * 按构造不可达(写口恒盖、M21 补齐);计数非零 = 写腿或手改出了错,按 `SELECT … WHERE status = 'pending' AND gate_kind IS NULL` 查那一行。
289
+ */
290
+ export declare function queueRowGateKind(kind: unknown): CheckpointGate["kind"] | undefined;
281
291
  /** Redact + size-bound a pending tool call's args for persistence/serving. JSON-roundtrips so the stored shape
282
292
  * matches what the operator queue serves; oversized payloads collapse to a marker rather than bloating the row.
283
293
  * Exported for the LOCAL twin (local-checkpoint-store.ts) so the operator-queue projection can't drift. */
@@ -1,13 +1,13 @@
1
1
  import { parkedApprovalCard, parkedFacetOf } from "../approval-content-kind.js";
2
2
  import { createHash } from "node:crypto";
3
3
  import { APPROVAL_GATE_KINDS_SQL_IN, denySweepOwns } from "../tool-approval.js";
4
- import { CheckpointError, MAX_RULE_TEXT_CHARS, validatePendingSteer, appendPendingSteer, checkpointVersionOf, winnerFromOutcome, summarizeCheckpoint, MAX_SUPPORTED_CHECKPOINT_VERSION, ExecutionOutcomeConflictError, executionRecordDisposition, executionVerdict, } from "@sema-agent/core";
4
+ import { CheckpointError, isGateKind, MAX_RULE_TEXT_CHARS, validatePendingSteer, appendPendingSteer, checkpointVersionOf, winnerFromOutcome, summarizeCheckpoint, MAX_SUPPORTED_CHECKPOINT_VERSION, ExecutionOutcomeConflictError, executionRecordDisposition, executionVerdict, } from "@sema-agent/core";
5
5
  import { redactDeep, redactSecrets } from "../trace/redact.js";
6
6
  import { MAX_RULE_OFFERS, MAX_RULE_OFFER_BATCH_MEMBERS, RuleOfferRawSchema, backfillLegacyBatchMemberKind, parkRowFacts, screenUncoveredDetailSeat } from "../approval-card.js";
7
7
  import { assertJsonColumnWritable, createObjectColumnReader, decodeJsonColumn, decodeNullableJsonColumn, foldJsonColumn, isJsonColumnBodyProblem, jsonColumnText, readJsonArrayColumn, readJsonObjectColumn, readNonNullJsonValueColumn, } from "./sql-json-column.js";
8
8
  import { recordFailOpen } from "../observability/fail-open.js";
9
9
  import { mysqlDriver, pgDriver, dialectProtocolJsonEncoder } from "./sql-driver.js";
10
- import { isDupKeyError, isMissingColumnError } from "./sql-errors.js";
10
+ import { isDupKeyError, isMissingColumnError, rowTooLargeOrSelf } from "./sql-errors.js";
11
11
  import { MANAGED_RETENTION } from "./retention-store-sql.js";
12
12
  import { approvalPayloadFingerprint } from "./approval-payload-fingerprint.js";
13
13
  const REAP_CLASSIFY_BATCH = 200;
@@ -196,6 +196,12 @@ function readParkGateCell(cell) {
196
196
  }
197
197
  }
198
198
  const readParkGateMemberBody = createObjectColumnReader();
199
+ export function queueRowGateKind(kind) {
200
+ if (isGateKind(kind))
201
+ return kind;
202
+ recordFailOpen("server.approvals.queue-row-gate-kind-missing", `gate_kind=${kind === null || kind === undefined ? "NULL" : redactSecrets(String(kind)).slice(0, 40)}`);
203
+ return undefined;
204
+ }
199
205
  function parkedGateOfRow(r) {
200
206
  return { kind: r.gate_kind ?? undefined, approvalCard: readParkGateCell(r[PARK_ROW_GATE.as])?.approvalCard };
201
207
  }
@@ -418,7 +424,7 @@ export class SqlCheckpointStore {
418
424
  if (this.isDupKey(e)) {
419
425
  throw new CheckpointError("checkpoint.already_exists", `checkpoint token already exists`);
420
426
  }
421
- throw e;
427
+ throw rowTooLargeOrSelf(this.db.dialect, e, `the checkpoint row for session ${JSON.stringify(cp.sessionId)}`);
422
428
  }
423
429
  }
424
430
  async get(token) {
@@ -582,31 +588,33 @@ export class SqlCheckpointStore {
582
588
  const { rows } = scope
583
589
  ? await this.db.query(`${base}${this.q(" AND c.scope=?", " AND c.scope=$1")} ORDER BY c.created_at_ms ASC`, [scope])
584
590
  : await this.db.query(`${base} ORDER BY c.created_at_ms ASC`);
585
- const out = rows.map((r) => {
591
+ const out = rows.flatMap((r) => {
592
+ const gateKind = queueRowGateKind(r.gate_kind);
593
+ if (gateKind === undefined)
594
+ return [];
586
595
  const toolName = r.tool_name ?? null;
587
596
  const toolCallId = r.tool_call_id ?? null;
588
597
  const boundInputHash = r.bound_input_hash ?? null;
589
- const gateKind = r.gate_kind ?? null;
590
598
  const ruleOffers = readRuleOffersCell(r.rule_suggestions);
591
599
  const approvalCard = parkedApprovalCard(parkedGateOfRow(r));
592
- return {
593
- sessionId: String(r.session_id),
594
- scope: String(r.scope),
595
- toolName,
596
- toolCallId,
597
- ...(toolCallId !== null ? { boundCallId: toolCallId } : {}),
598
- ...(boundInputHash !== null ? { boundInputHash } : {}),
599
- ...(gateKind !== null ? { gateKind } : {}),
600
- taskId: r.task_id ?? null,
601
- input: refusing("a tool input the operator would be approving unseen", () => decodeToolInputColumn(r.tool_input)) ?? null,
602
- createdAt: Number(r.created_at_ms),
603
- deadline: r.deadline == null ? null : Number(r.deadline),
604
- riskDescriptor: readRiskDescriptorCell(r.risk_descriptor),
605
- ...(ruleOffers !== undefined ? { ruleOffers } : {}),
606
- ...(Number(r.has_bidi_controls) === 1 ? { hasBidiControls: true } : {}),
607
- ...(approvalCard !== undefined ? { approvalCard } : {}),
608
- ...readParkRowFactsCell(r[PARK_ROW_PENDING_ACTION.as]),
609
- };
600
+ return [{
601
+ sessionId: String(r.session_id),
602
+ scope: String(r.scope),
603
+ toolName,
604
+ toolCallId,
605
+ ...(toolCallId !== null ? { boundCallId: toolCallId } : {}),
606
+ ...(boundInputHash !== null ? { boundInputHash } : {}),
607
+ gateKind,
608
+ taskId: r.task_id ?? null,
609
+ input: refusing("a tool input the operator would be approving unseen", () => decodeToolInputColumn(r.tool_input)) ?? null,
610
+ createdAt: Number(r.created_at_ms),
611
+ deadline: r.deadline == null ? null : Number(r.deadline),
612
+ riskDescriptor: readRiskDescriptorCell(r.risk_descriptor),
613
+ ...(ruleOffers !== undefined ? { ruleOffers } : {}),
614
+ ...(Number(r.has_bidi_controls) === 1 ? { hasBidiControls: true } : {}),
615
+ ...(approvalCard !== undefined ? { approvalCard } : {}),
616
+ ...readParkRowFactsCell(r[PARK_ROW_PENDING_ACTION.as]),
617
+ }];
610
618
  });
611
619
  return out.sort((a, b) => (b.riskDescriptor?.severity ?? 0) - (a.riskDescriptor?.severity ?? 0) || a.createdAt - b.createdAt);
612
620
  }
@@ -716,9 +724,14 @@ export class SqlCheckpointStore {
716
724
  async putCtx(sessionId, ctx) {
717
725
  const text = this.json(ctx, "checkpoint ctx");
718
726
  assertJsonColumnWritable(text, createObjectColumnReader(), `checkpoint_ctx.ctx for session ${JSON.stringify(sessionId)} (write leg)`);
719
- await this.db.query(this.q("INSERT INTO checkpoint_ctx (session_id, ctx, updated_at_ms) VALUES (?,?,?) " +
720
- "ON DUPLICATE KEY UPDATE ctx = VALUES(ctx), updated_at_ms = VALUES(updated_at_ms)", "INSERT INTO checkpoint_ctx (session_id, ctx, updated_at_ms) VALUES ($1,$2::jsonb,$3) " +
721
- "ON CONFLICT (session_id) DO UPDATE SET ctx = EXCLUDED.ctx, updated_at_ms = EXCLUDED.updated_at_ms"), [sessionId, text, Date.now()]);
727
+ try {
728
+ await this.db.query(this.q("INSERT INTO checkpoint_ctx (session_id, ctx, updated_at_ms) VALUES (?,?,?) " +
729
+ "ON DUPLICATE KEY UPDATE ctx = VALUES(ctx), updated_at_ms = VALUES(updated_at_ms)", "INSERT INTO checkpoint_ctx (session_id, ctx, updated_at_ms) VALUES ($1,$2::jsonb,$3) " +
730
+ "ON CONFLICT (session_id) DO UPDATE SET ctx = EXCLUDED.ctx, updated_at_ms = EXCLUDED.updated_at_ms"), [sessionId, text, Date.now()]);
731
+ }
732
+ catch (e) {
733
+ throw rowTooLargeOrSelf(this.db.dialect, e, `checkpoint_ctx.ctx for session ${JSON.stringify(sessionId)}`);
734
+ }
722
735
  }
723
736
  async getCtx(sessionId) {
724
737
  const { rows } = await this.db.query(this.q(`SELECT ${jsonColumnText("tidb", "ctx")} FROM checkpoint_ctx WHERE session_id=?`, `SELECT ${jsonColumnText("pg", "ctx")} FROM checkpoint_ctx WHERE session_id=$1`), [sessionId]);