@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
@@ -24,9 +24,34 @@
24
24
  * 不在这里 —— 在 `sql-driver.ts` 的连接初始化里(TiDB 乐观模式设不成悲观 / 隔离级复核不符 ⇒ 响亮抛)。
25
25
  * 本模块在 boot 期借一条连接**触发**那次初始化,于是那句拒绝落在启动期而不是第一次业务写上。
26
26
  */
27
- import { type SqlEngineFacts } from "./plugins/sql-driver.js";
27
+ import { type SqlEngineFacts, type SqlEngineReading } from "./plugins/sql-driver.js";
28
+ import type { StoreBackend } from "./plugins/store-backend.js";
28
29
  import type { Pool as MySqlPool } from "mysql2/promise";
29
30
  import type { Pool as PgPool } from "pg";
31
+ /**
32
+ * 一只店后端交出的 SQL 池(至多一只:`mysqlPool()` 只在 MySQL 协议后端上有值,`pgPool()` 只在 PG 后端上;local 后端 / 没有后端 ⇒ 两个都 `undefined`)。
33
+ * **「SQL 后端在场」只有这一个谓词**:boot 期据它建立引擎读口({@link establishSqlEnginePosture}),`createHttpServer` 据它判「读口必填」——
34
+ * 两处读同一只函数,所以本服务自己的装配(有池 ⇒ 有读口)恒满足那道起服门,嵌入方少给读口则被它拒。
35
+ */
36
+ export declare function sqlPoolsOf(backend: Pick<StoreBackend, "mysqlPool" | "pgPool"> | undefined): {
37
+ mysqlPool: MySqlPool | undefined;
38
+ pgPool: PgPool | undefined;
39
+ };
40
+ /**
41
+ * `ServiceDeps.sqlEngine` 座的**值形**判:`{ dialect: "tidb" | "pg", facts }`,`facts` 是 `null`(池上连接初始化还没成功)或一份引擎事实 —— 判到墙判官 `sqlWallsOf` 读的那几位
42
+ * (`maxRowBytes` / `maxPacketBytes` 是 `null` 或数,`wallSources` 两位是出处词)。一个函数(7.131.x 的事实读口 `() => facts` 只改了键名)、缺 `facts` 的对象、错方言词都不是它。
43
+ * `facts` 是活读(getter):这里读一次,判的是装配这一刻看得到的那一份。
44
+ */
45
+ export declare function isSqlEngineReading(v: unknown): v is SqlEngineReading;
46
+ /** 引擎读口座的装配问题(闭集;{@link sqlEngineSeatIssue} 的产物):`required` = SQL 后端在场而座缺席;`bad_shape` = 座在场但形不对。 */
47
+ export type SqlEngineSeatIssue = "required" | "bad_shape";
48
+ /**
49
+ * 🔴 **引擎读口座的装配判官**(`createHttpServer` 起服时调;两臂一只函数):
50
+ * · 座在场 ⇒ 判值形({@link isSqlEngineReading}),不对 ⇒ `bad_shape`;
51
+ * · 座缺席 ⇒ 看后端交没交出 SQL 池({@link sqlPoolsOf},与 boot 期建立读口同一个谓词):交了 ⇒ `required`(席缺席只表示「没有 SQL 后端」,不能被判官读成「没有墙」);没交 ⇒ 过。
52
+ * 都是 fail-closed:墙门只认这一席,席坏了或漏了,代价是每份超出库墙的体照样受理或每次提交 500,不是起服时一句话。
53
+ */
54
+ export declare function sqlEngineSeatIssue(backend: Pick<StoreBackend, "mysqlPool" | "pgPool"> | undefined, sqlEngine: unknown): SqlEngineSeatIssue | undefined;
30
55
  /** 消费端窄投影(`GET /v1/capabilities` 的 `store.sql` 位;7.106.0 前是顶层 `sql`,S-866 ①)。`null` = 驱动此刻给不出事实 ⇒ `store.sql` 缺席。 */
31
56
  export interface SqlEngineCapability {
32
57
  engine: SqlEngineFacts["engine"];
@@ -34,8 +59,8 @@ export interface SqlEngineCapability {
34
59
  txnMode: SqlEngineFacts["txnMode"];
35
60
  }
36
61
  /**
37
- * Boot 期建立引擎事实:借一条池连接(初始化随之跑),再原样归还。返回一个 **live getter** —— 事实由
38
- * 驱动持有,重连后它会被下一次初始化刷新,快照会说谎。
62
+ * Boot 期建立引擎事实:借一条池连接(初始化随之跑),再原样归还。返回本池的**引擎读口**(驱动本身:方言 + 活的 `facts`)—— 事实由
63
+ * 驱动持有,重连后它会被下一次初始化刷新,快照会说谎;方言与事实同出这一只,写入要装进的墙(`sqlWallsOf`)只读它。
39
64
  *
40
65
  * 抛出 = 拒启:连接初始化拒绝了这个后端(TiDB 乐观模式设不成悲观、隔离级复核不符、服务器太旧读不出
41
66
  * `@@transaction_isolation`)。这条错**刻意不吞** —— 本仓所有 `FOR UPDATE` 判据都以它为前提。
@@ -43,7 +68,7 @@ export interface SqlEngineCapability {
43
68
  export declare function establishSqlEnginePosture(pools: {
44
69
  mysqlPool?: MySqlPool | undefined;
45
70
  pgPool?: PgPool | undefined;
46
- }): Promise<(() => SqlEngineFacts | null) | undefined>;
71
+ }): Promise<SqlEngineReading | undefined>;
47
72
  /** operator 面 → 消费端面的窄投影(`version` 是运维料,刻意不进能力位:它只对排障有意义)。 */
48
73
  export declare function projectSqlEngineCapability(facts: SqlEngineFacts | null | undefined): SqlEngineCapability | null;
49
74
  //# sourceMappingURL=sql-engine-posture.d.ts.map
@@ -1,11 +1,46 @@
1
1
  import { mysqlDriver, pgDriver } from "./plugins/sql-driver.js";
2
+ export function sqlPoolsOf(backend) {
3
+ return { mysqlPool: backend?.mysqlPool?.(), pgPool: backend?.pgPool?.() };
4
+ }
5
+ const DIALECT_WORDS = { tidb: true, pg: true };
6
+ const WALL_SOURCE_WORDS = { reported: true, assumed: true };
7
+ const isWallBytes = (v) => v === null || (typeof v === "number" && Number.isFinite(v) && v >= 0);
8
+ const isWallSourceWord = (v) => typeof v === "string" && Object.hasOwn(WALL_SOURCE_WORDS, v);
9
+ export function isSqlEngineReading(v) {
10
+ if (typeof v !== "object" || v === null)
11
+ return false;
12
+ const r = v;
13
+ if (typeof r.dialect !== "string" || !Object.hasOwn(DIALECT_WORDS, r.dialect))
14
+ return false;
15
+ if (!("facts" in v))
16
+ return false;
17
+ const facts = v.facts;
18
+ if (facts === null)
19
+ return true;
20
+ if (typeof facts !== "object")
21
+ return false;
22
+ const f = facts;
23
+ if (!isWallBytes(f.maxRowBytes) || !isWallBytes(f.maxPacketBytes))
24
+ return false;
25
+ const sources = f.wallSources;
26
+ if (typeof sources !== "object" || sources === null)
27
+ return false;
28
+ const src = sources;
29
+ return isWallSourceWord(src.maxRowBytes) && isWallSourceWord(src.maxPacketBytes);
30
+ }
31
+ export function sqlEngineSeatIssue(backend, sqlEngine) {
32
+ if (sqlEngine !== undefined)
33
+ return isSqlEngineReading(sqlEngine) ? undefined : "bad_shape";
34
+ const { mysqlPool, pgPool } = sqlPoolsOf(backend);
35
+ return mysqlPool !== undefined || pgPool !== undefined ? "required" : undefined;
36
+ }
2
37
  export async function establishSqlEnginePosture(pools) {
3
38
  const driver = pools.mysqlPool ? mysqlDriver(pools.mysqlPool) : pools.pgPool ? pgDriver(pools.pgPool) : undefined;
4
39
  if (driver === undefined)
5
40
  return undefined;
6
41
  const conn = await driver.connect();
7
42
  conn.release();
8
- return () => driver.facts;
43
+ return driver;
9
44
  }
10
45
  export function projectSqlEngineCapability(facts) {
11
46
  return facts == null ? null : { engine: facts.engine, isolation: facts.isolation, txnMode: facts.txnMode };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 一次性迁移 M19 —— 续跑重放腿上,升级前存下的请求体里引擎判官判不过的 `mcpServers[].toolAxes`,窗内按升级前的读法重读一次。
2
+ * 一次性迁移 M19 —— 续跑重放腿上,升级前存下的请求体里引擎判官判不过的 `mcpServers[].toolAxes` 与 `a2aPeers[].toolAxes`,窗内按升级前的读法重读一次。
3
3
  * 🔴 M19 · 窗口见 `plugins/one-time-migrations.ts` 的 ONE_TIME_MIGRATIONS 登记表(窗、删法、删后由谁接住只写在那一行)。
4
4
  *
5
5
  * ## 为什么要它(持久数据面)
@@ -9,17 +9,32 @@
9
9
  * ⇒ 永远批不过去。持久数据面不能「翻红」,所以给它一次性迁移:窗开着时,续跑重放腿把判官判不过的那一格按升级前的读法重读一次,产物**再交同一只判官**
10
10
  * (不过 ⇒ 原臂:整条不进);真挂上的这类 server 由请求腿说一行运维 warn `mcp_tool_axes_read_as_before_upgrade`。窗关之后回到今天的读法,删码零行为变化。
11
11
  *
12
+ * ## A2A 一面(同一种病,同一只重读座;core 7.53.0 #1835 提货批)
13
+ * core 7.53.0 起 A2A 请求腿的 `toolAxes` 读者也是引擎的判官(`readToolAxes(…, "a2a")`)。7.131.x 的 A2A 请求腿读者对每只 skill 的记录只挑三个轴、**多余成员静默忽略**、
14
+ * 照挂 ⇒ 存量体里带多余成员的那条对端,续跑重放时整条不进,停驻在它某只 skill 审批上的任务同样永远批不过去。同一只重读座覆盖它,律只有一句:**重读复现那一面升级前
15
+ * 的读法,一个成员都不多救** —— A2A 那一面交 {@link readA2aToolAxesAsServer7131M19}(7.131.x 的读者逐字:未知成员忽略;已知轴写坏 —— `effect` / `irreversibility` 出词表、
16
+ * `egress` 非布尔、某只 skill 的值不是对象、整份不是对象含 `null` —— ⇒ 那条对端当时就整条丢,重读照旧整条丢,不交判官),产物再交同一只判官;
17
+ * 真挂上的对端一行运维 warn `a2a_tool_axes_read_as_before_upgrade`。所以 7.131.x 当时没挂上的对端,窗内续跑也不挂(不扩大外部工具集)。
18
+ *
12
19
  * ## 可达面(结构上只在续跑重放腿)
13
20
  * · 本模块的值只被 `boot/resolve-spec.ts` 引用一处,挂在 `opts.leg` 不是 `"fresh"` 的那一臂(续跑家族经 `http/resume-legs.ts` 的 `resolveResumeSpec`
14
21
  * 递 `leg: "resume"`);新提交腿递 `"fresh"` 拿不到重读座,且在腿首已经被同一只判官 400 掉(`assertRequestMcpEntries`),读器根本见不到坏形;
15
- * · 请求腿(`task-mcp.ts`)只 import 本模块的**型**:重读座是 `resolveRequestMcp` 的一个形参,没人递就没有;探测口(`validateRequestMcp`)没有这个形参;
22
+ * · 两条请求腿(`task-mcp.ts` / `task-a2a.ts`)只 import 本模块的**型**:重读座是 `resolveRequestMcp` / `resolveRequestA2a` 的一个形参,没人递就没有(`resolve-spec.ts`
23
+ * 求值一次、两腿同递那一份);探测口(`validateRequestMcp`)与 `validateRequestA2a` 没有这个形参;
16
24
  * · 部署腿(`web-admin-client/skills-mcp.ts` 的 `resolveMcpServers`)是另一只铸 spec 处,不读本模块。
17
25
  * 机器锚:`test/mcp-tool-axes-m19.test.ts` 的结构格。
18
26
  */
19
27
  import type { McpServerSpec } from "@sema-agent/core";
20
28
  type ToolAxes = NonNullable<McpServerSpec["toolAxes"]>;
21
- /** 重读座:一份原始 `toolAxes` 声明 → 升级前的读法读出的那一份(`undefined` = 升级前读成「没声明」)。产物由调用方再交引擎判官。 */
22
- export type StoredToolAxesRereadM19 = (declared: unknown) => ToolAxes | undefined;
29
+ /**
30
+ * 重读座:一份原始 `toolAxes` 声明 → **那一面**升级前的读法读出的那一份,产物由调用方再交引擎判官。两面各一只读者,因为两面升级前的读法不同:
31
+ * · `mcp`(≤7.129.x 的 MCP 请求腿):`undefined` = 升级前读成「没声明」(server 照挂);升级前从不因 `toolAxes` 丢 server;
32
+ * · `a2a`(7.131.x 的 A2A 请求腿):`undefined` 同上;`null` = 7.131.x 当时就把这条对端整条丢了 —— 重读不救活,调用方按原臂整条不进。
33
+ */
34
+ export interface StoredToolAxesRereadM19 {
35
+ readonly mcp: (declared: unknown) => ToolAxes | undefined;
36
+ readonly a2a: (declared: unknown) => ToolAxes | undefined | null;
37
+ }
23
38
  /**
24
39
  * 此刻(`today` = UTC 日历日 YYYY-MM-DD)M19 的窗开着 ⇒ 交回重读座;关了 ⇒ `undefined`(今天的读法)。窗只读登记表 M19 那一行
25
40
  * (`migrationWindowOpen`,与 M10 的认领窗同一只判据)。登记表里没有 M19 ⇒ 响亮抛:重读臂活过了它的登记行,该随那一行一起删。
@@ -1,5 +1,8 @@
1
1
  import { ONE_TIME_MIGRATIONS, migrationWindowOpen } from "./plugins/one-time-migrations.js";
2
- import { isToolAxisIrreversibility, isToolEffect } from "./tool-axis-words.js";
2
+ const TOOL_EFFECT_WORDS_M19 = ["read", "write", "idempotent"];
3
+ const TOOL_AXIS_IRREVERSIBILITY_WORDS_M19 = ["always", "never"];
4
+ const isToolEffect = (v) => typeof v === "string" && TOOL_EFFECT_WORDS_M19.includes(v);
5
+ const isToolAxisIrreversibility = (v) => typeof v === "string" && TOOL_AXIS_IRREVERSIBILITY_WORDS_M19.includes(v);
3
6
  function readToolAxesAsServer7129M19(declared) {
4
7
  if (!declared || typeof declared !== "object" || Array.isArray(declared))
5
8
  return undefined;
@@ -18,10 +21,35 @@ function readToolAxesAsServer7129M19(declared) {
18
21
  }
19
22
  return Object.keys(axes).length > 0 ? axes : undefined;
20
23
  }
24
+ function readA2aToolAxesAsServer7131M19(declared) {
25
+ if (!declared || typeof declared !== "object" || Array.isArray(declared))
26
+ return null;
27
+ const axes = {};
28
+ for (const [skill, ax] of Object.entries(declared)) {
29
+ if (!ax || typeof ax !== "object" || Array.isArray(ax))
30
+ return null;
31
+ const a = ax;
32
+ if (a.effect !== undefined && !isToolEffect(a.effect))
33
+ return null;
34
+ if (a.egress !== undefined && typeof a.egress !== "boolean")
35
+ return null;
36
+ if (a.irreversibility !== undefined && !isToolAxisIrreversibility(a.irreversibility))
37
+ return null;
38
+ const entry = {
39
+ ...(isToolEffect(a.effect) ? { effect: a.effect } : {}),
40
+ ...(typeof a.egress === "boolean" ? { egress: a.egress } : {}),
41
+ ...(isToolAxisIrreversibility(a.irreversibility) ? { irreversibility: a.irreversibility } : {}),
42
+ };
43
+ if (Object.keys(entry).length > 0)
44
+ axes[skill] = entry;
45
+ }
46
+ return Object.keys(axes).length > 0 ? axes : undefined;
47
+ }
48
+ const REREAD_M19 = { mcp: readToolAxesAsServer7129M19, a2a: readA2aToolAxesAsServer7131M19 };
21
49
  export function storedToolAxesRereadM19(today) {
22
50
  const window = ONE_TIME_MIGRATIONS.find((w) => w.id === "M19");
23
51
  if (window === undefined)
24
52
  throw new Error("stored-request-tool-axes-m19: the one-time migration registry has no M19 row (the replay-leg toolAxes re-read window) — the re-read arm outlived its registry row; delete it with that row");
25
- return migrationWindowOpen(window, today) ? readToolAxesAsServer7129M19 : undefined;
53
+ return migrationWindowOpen(window, today) ? REREAD_M19 : undefined;
26
54
  }
27
55
  //# sourceMappingURL=stored-request-tool-axes-m19.js.map
@@ -42,6 +42,7 @@
42
42
  * 「广告了却拦不住」的那一类缺口。钉在 `test/task-a2a.test.ts`(含到货后该怎么改那一格的原地说明)。
43
43
  */
44
44
  import { type A2aServerSpec, type CompliancePosture } from "@sema-agent/core";
45
+ import type { StoredToolAxesRereadM19 } from "./stored-request-tool-axes-m19.js";
45
46
  /** Bound on caller-supplied peers (anti-DoS; a real declaration names a handful of agents). */
46
47
  export declare const MAX_REQUEST_A2A_PEERS = 32;
47
48
  /** The deployment-facing shape this module reads. `lockedConfigKeys` is deliberately `readonly string[]`
@@ -69,6 +70,8 @@ export interface A2aGateConfig {
69
70
  * deployment with a broken posture keep advertising the capability (仓规:安全轴禁静默兜底).
70
71
  */
71
72
  export declare function a2aInjectionHonored(config: A2aGateConfig): boolean;
73
+ /** 读这条对端的是哪条腿:`fresh` = 新提交;`resume` = 续跑重放升级前存下的请求体(`boot/resolve-spec.ts` 的 `opts.leg` 分流,同一个读法)。 */
74
+ type A2aRequestLeg = "fresh" | "resume";
72
75
  /**
73
76
  * Shape-check the raw `body.a2aPeers`. `null` → a non-array (caller error; the route may 400). Otherwise the
74
77
  * valid subset + the names of dropped (malformed/over-cap) entries.
@@ -110,5 +113,10 @@ export declare function mergeRequestA2a(baseline: A2aServerSpec[] | undefined, g
110
113
  export declare function resolveRequestA2a(baseline: A2aServerSpec[] | undefined, bodyA2a: unknown, config: A2aGateConfig, logger?: {
111
114
  info?(m: string, meta?: unknown): void;
112
115
  warn(m: string, meta?: unknown): void;
113
- }): A2aServerSpec[] | undefined;
116
+ },
117
+ /** 哪条腿在读(`boot/resolve-spec.ts` 按 `opts.leg` 递;缺省 = 新提交,最严的读法):只决定 `headers` 里 `__proto__` 名成员的处置(见 {@link normalizeA2aPeer})。 */
118
+ leg?: A2aRequestLeg,
119
+ /** 一次性迁移 M19 的重读座(窗口见 `plugins/one-time-migrations.ts` 登记表):只在续跑重放腿、只在窗开着时由 `boot/resolve-spec.ts` 递进来 —— 与 MCP 请求腿同一份。 */
120
+ storedToolAxes?: StoredToolAxesRereadM19): A2aServerSpec[] | undefined;
121
+ export {};
114
122
  //# sourceMappingURL=task-a2a.d.ts.map
package/dist/task-a2a.js CHANGED
@@ -1,7 +1,8 @@
1
- import { resolveComplianceDenies } from "@sema-agent/core";
2
- import { isUsableWireName } from "./wire-name-bound.js";
1
+ import { readToolAxes, resolveComplianceDenies } from "@sema-agent/core";
2
+ import { isUsableWireName } from "@sema-agent/settings-schema";
3
3
  import { HttpError } from "./security.js";
4
- import { isToolAxisIrreversibility, isToolEffect } from "./tool-axis-words.js";
4
+ import { readPlainStringRecord } from "./plain-string-record.js";
5
+ import { echoKeyName } from "./request-key-closure.js";
5
6
  import { GOVERNANCE_HTTP_STATUS } from "./governance-http-status.js";
6
7
  export const MAX_REQUEST_A2A_PEERS = 32;
7
8
  export function a2aInjectionHonored(config) {
@@ -14,79 +15,83 @@ export function a2aInjectionHonored(config) {
14
15
  }
15
16
  return config.requirePrincipal !== true;
16
17
  }
17
- function isStringRecord(v) {
18
- return !!v && typeof v === "object" && !Array.isArray(v) && Object.values(v).every((x) => typeof x === "string");
19
- }
20
18
  const HTTP_URL_RE = /^https?:\/\//i;
21
- function normalizeA2aPeer(e) {
19
+ function normalizeA2aPeer(e, leg, storedToolAxes) {
20
+ const dropped = { spec: null };
22
21
  if (!e || typeof e !== "object")
23
- return null;
22
+ return dropped;
24
23
  const o = e;
25
24
  if (!isUsableWireName(o.name))
26
- return null;
25
+ return dropped;
27
26
  if (typeof o.url !== "string" || !HTTP_URL_RE.test(o.url))
28
- return null;
27
+ return dropped;
29
28
  if (o.cardUrl !== undefined && (typeof o.cardUrl !== "string" || !HTTP_URL_RE.test(o.cardUrl)))
30
- return null;
31
- if (o.headers !== undefined && !isStringRecord(o.headers))
32
- return null;
29
+ return dropped;
30
+ const headers = o.headers === undefined ? undefined : readPlainStringRecord(o.headers);
31
+ if (headers !== undefined && (!headers.ok || (headers.stripped.length > 0 && leg === "fresh")))
32
+ return dropped;
33
+ const lost = headers?.ok === true && headers.stripped.length > 0 ? { headersLost: headers.stripped.map((m) => echoKeyName("headers.", m.name)) } : {};
33
34
  if (o.principalHeader !== undefined && typeof o.principalHeader !== "string")
34
- return null;
35
+ return dropped;
35
36
  if (o.allowSkills !== undefined && !(Array.isArray(o.allowSkills) && o.allowSkills.every((s) => typeof s === "string")))
36
- return null;
37
+ return dropped;
37
38
  const spec = { name: o.name, url: o.url };
38
39
  if (typeof o.cardUrl === "string")
39
40
  spec.cardUrl = o.cardUrl;
40
- if (isStringRecord(o.headers))
41
- spec.headers = o.headers;
41
+ if (headers?.ok === true)
42
+ spec.headers = headers.record;
42
43
  if (typeof o.principalHeader === "string")
43
44
  spec.principalHeader = o.principalHeader;
44
45
  if (Array.isArray(o.allowSkills))
45
46
  spec.allowSkills = o.allowSkills;
46
47
  if (o.toolAxes !== undefined) {
47
- if (!o.toolAxes || typeof o.toolAxes !== "object" || Array.isArray(o.toolAxes))
48
- return null;
49
- const axes = {};
50
- for (const [skill, ax] of Object.entries(o.toolAxes)) {
51
- if (!ax || typeof ax !== "object" || Array.isArray(ax))
52
- return null;
53
- const a = ax;
54
- if (a.effect !== undefined && !isToolEffect(a.effect))
55
- return null;
56
- if (a.egress !== undefined && typeof a.egress !== "boolean")
57
- return null;
58
- if (a.irreversibility !== undefined && !isToolAxisIrreversibility(a.irreversibility))
59
- return null;
60
- const entry = {
61
- ...(isToolEffect(a.effect) ? { effect: a.effect } : {}),
62
- ...(typeof a.egress === "boolean" ? { egress: a.egress } : {}),
63
- ...(isToolAxisIrreversibility(a.irreversibility) ? { irreversibility: a.irreversibility } : {}),
64
- };
65
- if (Object.keys(entry).length > 0)
66
- axes[skill] = entry;
48
+ const axes = readToolAxes(o.toolAxes, "a2a");
49
+ if (!axes.ok && storedToolAxes !== undefined) {
50
+ const before = storedToolAxes.a2a(o.toolAxes);
51
+ if (before === null)
52
+ return dropped;
53
+ const again = readToolAxes(before, "a2a");
54
+ if (!again.ok)
55
+ return dropped;
56
+ if (again.toolAxes !== undefined)
57
+ spec.toolAxes = again.toolAxes;
58
+ return { spec, toolAxesReread: axes.problem, ...lost };
67
59
  }
68
- if (Object.keys(axes).length > 0)
69
- spec.toolAxes = axes;
60
+ if (!axes.ok)
61
+ return dropped;
62
+ if (axes.toolAxes !== undefined)
63
+ spec.toolAxes = axes.toolAxes;
70
64
  }
71
- return spec;
65
+ return { spec, ...lost };
72
66
  }
73
67
  export function validateRequestA2a(raw) {
68
+ const v = validateRequestA2aEntries(raw, "fresh");
69
+ return v === null ? null : { ok: v.ok, dropped: v.dropped };
70
+ }
71
+ function validateRequestA2aEntries(raw, leg, storedToolAxes) {
74
72
  if (raw === undefined || raw === null)
75
- return { ok: [], dropped: [] };
73
+ return { ok: [], dropped: [], toolAxesReread: [], headersLost: [] };
76
74
  if (!Array.isArray(raw))
77
75
  return null;
78
76
  const ok = [];
79
77
  const dropped = [];
78
+ const toolAxesReread = [];
79
+ const headersLost = [];
80
80
  for (const e of raw.slice(0, MAX_REQUEST_A2A_PEERS)) {
81
- const v = normalizeA2aPeer(e);
82
- if (v)
81
+ const { spec: v, toolAxesReread: reread, headersLost: lost } = normalizeA2aPeer(e, leg, storedToolAxes);
82
+ if (v) {
83
83
  ok.push(v);
84
+ if (reread !== undefined)
85
+ toolAxesReread.push({ peer: v.name, problem: reread });
86
+ if (lost !== undefined)
87
+ headersLost.push({ peer: v.name, lost });
88
+ }
84
89
  else
85
90
  dropped.push(e && typeof e === "object" && typeof e.name === "string" ? String(e.name).slice(0, 128) : "<malformed>");
86
91
  }
87
92
  if (raw.length > MAX_REQUEST_A2A_PEERS)
88
93
  dropped.push(`(+${raw.length - MAX_REQUEST_A2A_PEERS} over cap of ${MAX_REQUEST_A2A_PEERS})`);
89
- return { ok, dropped };
94
+ return { ok, dropped, toolAxesReread, headersLost };
90
95
  }
91
96
  export function assertRequestA2aUnlocked(bodyA2a, lockedKeys) {
92
97
  if (!lockedKeys.has("a2a"))
@@ -104,20 +109,41 @@ export function mergeRequestA2a(baseline, gated) {
104
109
  const add = gated.filter((p) => !taken.has(p.name));
105
110
  return add.length === 0 ? baseline : [...(baseline ?? []), ...add];
106
111
  }
107
- export function resolveRequestA2a(baseline, bodyA2a, config, logger) {
112
+ export function resolveRequestA2a(baseline, bodyA2a, config, logger, leg = "fresh", storedToolAxes) {
108
113
  if (bodyA2a === undefined || bodyA2a === null)
109
114
  return baseline;
110
115
  if (!a2aInjectionHonored(config)) {
111
116
  logger?.warn("a2a_injection_ignored", { reason: "gate_closed (per-request A2A peers honored only on a single-user, unlocked, compliance-permitted deployment)" });
112
117
  return baseline;
113
118
  }
114
- const v = validateRequestA2a(bodyA2a);
119
+ const v = validateRequestA2aEntries(bodyA2a, leg, storedToolAxes);
115
120
  if (v === null) {
116
121
  logger?.warn("a2a_injection_ignored", { reason: "a2aPeers not an array" });
117
122
  return baseline;
118
123
  }
119
124
  if (v.dropped.length > 0)
120
125
  logger?.warn("a2a_injection_dropped", { dropped: v.dropped });
121
- return mergeRequestA2a(baseline, v.ok);
126
+ const merged = mergeRequestA2a(baseline, v.ok);
127
+ const baselineNames = new Set((baseline ?? []).map((p) => p.name));
128
+ for (const r of v.toolAxesReread) {
129
+ if (baselineNames.has(r.peer))
130
+ continue;
131
+ logger?.warn("a2a_tool_axes_read_as_before_upgrade", {
132
+ peer: r.peer,
133
+ migration: "M19",
134
+ problem: r.problem,
135
+ note: "this replayed request was stored before the upgrade and the peer's toolAxes declaration does not pass the engine's reader; while one-time migration M19 is open it is read the way the pre-upgrade reader reads it (members that do not fit are dropped) and the peer stays mounted — once the window closes the same request leaves this peer unmounted",
136
+ });
137
+ }
138
+ for (const h of v.headersLost) {
139
+ if (baselineNames.has(h.peer))
140
+ continue;
141
+ logger?.warn("a2a_peer_headers_members_dropped", {
142
+ peer: h.peer,
143
+ lost: h.lost,
144
+ note: "this replayed request carries a headers member named \"__proto__\" for this peer (the engine refuses that name); the peer stays mounted with every other header as written, the named member left out. A new submission with the same peer leaves it unmounted",
145
+ });
146
+ }
147
+ return merged;
122
148
  }
123
149
  //# sourceMappingURL=task-a2a.js.map
@@ -37,10 +37,9 @@ export declare const MAX_REQUEST_MCP_SERVERS = 32;
37
37
  * ——同一条链上后续的正常事件全部排在它们后面。修前的名字数组把超上限折成**一条**聚合串,所以无界的
38
38
  * 是本批新写的那两个循环。
39
39
  *
40
- * 为什么不改成「数组太长就 400」(codex 的另一条建议,**未采纳**):今天 33 台的请求是**照常受理、挂前
41
- * 32 台**的,加一条新的拒绝面为了一个资源边界去改受理语义,爆炸半径比病本身大(而且 resume 腿重放存量
42
- * body 会当场砖死一批已 park 的任务——本仓 `opts.leg` 三处先例逐字写着「砖死 resume 比降级糟」)。
43
- * 有界点名两头都占:该说的仍然说得出,量级钉死在与挂载同阶。
40
+ * 新提交腿上超过 {@link MAX_REQUEST_MCP_SERVERS} 条的列表在腿首整份 400({@link requestMcpDeclarationRefusal}),走不到这里;
41
+ * 有界点名守的是**重放存量 body** 的腿(那条腿不拒 —— 砖死 resume 比降级糟,本仓 `opts.leg` 三处先例)与闸关腿:
42
+ * 存量 body 的长度只受当时的 HTTP 体上限约束,该说的仍然说得出,量级钉死在与挂载同阶。
44
43
  */
45
44
  export declare const MAX_REQUEST_MCP_DROP_NAMES = 32;
46
45
  /**
@@ -58,30 +57,20 @@ export declare const MAX_REQUEST_MCP_DROP_NAMES = 32;
58
57
  * (唯一一格不在 core 型上的键 = http 传输的 `assertionHeader`:引擎尚无座,本仓承载 —— 那一支的门钉在
59
58
  * `mcp-assertion-header.ts` 的 {@link RequestMcpServerSpec} 上,引擎一旦长出同名座,那里的退役门当场红。)
60
59
  *
61
- * ## 两个消费点的处置**刻意不同**(同一张表,不同的门)
62
- * · 提交腿({@link normalizeServer})对词表外的键**宽容**(读不认识的键就当没有)—— 那条腿要能安全地
63
- * 重放存量 body,一个 4xx 打在 resume 上会永久砖死一条 parked 任务(本仓三处 `opts.leg` 先例);
64
- * · 探测口**响亮拒**(400,点名 `mcpServers[i].<键>`)—— 它问的是「这台到底能不能用」,把一个拼错的键
65
- * 悄悄忽略再回一份好看的名单,正是那条面要根除的假答案。
66
- * 两个门,一个词表。表在这里,门在各自的腿上。
60
+ * ## 一个回答者,两扇门;读器另算
61
+ * 「这份声明能不能原样用」只有 {@link requestMcpDeclarationRefusal} 一只回答者:词表外的键(条目层 / 本形 `transport` 层)⇒ `400 request.body_shape` 带 `unknownKeys`,
62
+ * fresh 提交腿({@link assertRequestMcpEntries})与探测口同码同句 —— 一个拼错的键被悄悄不读,声明方会以为它生效了。
63
+ * 读器({@link normalizeServer})对词表外的键仍**宽容**(读不认识的键就当没有):它只服务重放存量 body 的腿,一个 4xx 打在 resume 上会永久砖死一条 parked 任务(本仓三处 `opts.leg` 先例)。
67
64
  *
68
65
  * ## 受理键的判官表也钉在这张表上
69
- * fresh 提交腿对**受理键的值**同样响亮拒({@link assertRequestMcpEntries}):逐键判官表 {@link ENTRY_KEY_JUDGES} 是 `{ [K in 本表的键]: 判官 }` 的映射型 ——
70
- * 本表多一个键而没写判官 ⇒ 编译红,不会出现「这个键写坏了却被悄悄受理」的新键。词表外的键仍由读器宽容、由探测口闭集拒(上面两条不变)。
66
+ * 逐键判官表 {@link ENTRY_KEY_JUDGES} 是 `{ [K in 本表的键]: 判官 }` 的映射型 —— 本表多一个键而没写判官 ⇒ 编译红,不会出现「这个键写坏了却被悄悄受理」的新键。
71
67
  */
72
- export declare const MCP_SERVER_ENTRY_KEYS: readonly ["allowTools", "contentOrigin", "elicitation", "name", "source", "toolAxes", "toolFaces", "transport"];
68
+ export declare const MCP_SERVER_ENTRY_KEYS: readonly ["allowTools", "contentOrigin", "elicitation", "name", "source", "startupTimeoutMs", "toolAxes", "toolFaces", "transport"];
73
69
  /** `transport.kind:"stdio"` 的键闭集(两向钉在 core 那一支上)。 */
74
70
  export declare const STDIO_MCP_TRANSPORT_KEYS: readonly ["args", "command", "env", "kind"];
75
71
  /** `transport.kind:"http"` 的键闭集(同上)。`assertionHeader` 是本仓承载、引擎尚无座的一格(见 {@link RequestMcpServerSpec}):
76
72
  * 表两向钉在**请求面的 http 传输型**上 = 引擎的键 ∪ 这一格;引擎一旦长出同名座,`mcp-assertion-header.ts` 的退役门当场红。 */
77
73
  export declare const HTTP_MCP_TRANSPORT_KEYS: readonly ["assertionHeader", "headers", "kind", "principalHeader", "url"];
78
- /** 条目层:`{}` 与缺席等价的可选 Record 键。 */
79
- export declare const MCP_ENTRY_EMPTY_RECORD_KEYS: readonly ["toolAxes", "toolFaces"];
80
- /** 条目层:`[]` 与缺席等价的可选数组键。 */
81
- export declare const MCP_ENTRY_EMPTY_ARRAY_KEYS: readonly ["allowTools"];
82
- /** 传输层(两种合起来):`{}` / `[]` 与缺席等价的键。 */
83
- export declare const MCP_TRANSPORT_EMPTY_RECORD_KEYS: readonly ["env", "headers"];
84
- export declare const MCP_TRANSPORT_EMPTY_ARRAY_KEYS: readonly ["args"];
85
74
  /**
86
75
  * Does THIS deployment honor caller-supplied per-request MCP servers? TRUE for a SINGLE-USER deployment
87
76
  * (`requirePrincipal !== true`) — the requester is the super-admin of their own worker, so their `.mcp.json` is
@@ -152,54 +141,53 @@ export declare function assertRequestMcpUnlocked(bodyMcp: unknown, lockedKeys: R
152
141
  /** 一条**原始**请求条目上两个身份头的问题类(没有问题 / 不是对象 / 不是 http 传输 ⇒ `undefined`)—— 提交腿的门、探测口、重放腿的披露三处共读,
153
142
  * 判据本体在 `mcp-assertion-header.ts`;这里只负责把「条目 → 传输对象」那一步读出来。 */
154
143
  export declare function requestEntryIdentityIssue(entry: unknown): McpIdentityHeadersIssue | undefined;
155
- /** 问题类 → wire 拒面(码 + 句子):两扇拒绝门(提交腿 / 探测口)同一份。`both_set` 是键**组合**不合法(`request.body_shape`),句子对契约包解析期原句逐字,条目名过唯一的回显清洗
156
- * (剥控制 / 双向字符 + 截长;读不出名字的条目用与丢弃点同一个占位);`assertionHeader` 非串是该键的值不合法,与其余所有受理键同一个拒形({@link entryFieldRefusal},点名 `<at>.transport.assertionHeader`)。 */
157
- export declare function requestEntryIdentityRefusal(entry: unknown, at: string): {
158
- errorCode: "request.body_shape" | "request.field_invalid";
159
- message: string;
160
- } | undefined;
144
+ /** 请求面 `mcpServers` 的一次拒:码 + 句 + 词表外的键名表(只在闭集那一形上在场)。两扇门(fresh 提交腿 / 探测口)同一份,状态恒 400。 */
145
+ export interface RequestMcpRefusal {
146
+ readonly errorCode: "request.body_shape" | "request.field_invalid";
147
+ readonly message: string;
148
+ /** 词表外的键,全路径形(`mcpServers[0].transport.header`;已清洗 + 截长 + 排序 + 封顶)。 */
149
+ readonly unknownKeys?: readonly string[];
150
+ }
161
151
  /**
162
- * S-1629 —— 请求体 `mcpServers[i]` **每个受理键**的值域门(fresh 腿限定,同步 400):**一条律**,取代此前按键各开一扇的三道门
163
- * (`contentOrigin` / 身份头 / `toolAxes`)与其余键上「整条悄悄摘掉」「悄悄丢键」两种沉默。
164
- *
165
- * ## 律
166
- * fresh 提交腿上,条目的**任一受理键**写坏 ⇒ `400 request.field_invalid`,拒句点名 `mcpServers[i].<键路径>`、server 名与**该键判官的原因句**。
167
- * 不挂一半、不丢声明、不默认:调用方是请求体作者,同一扇门对同一类错误只有一种答案。修前同一条条目上有三种(`toolAxes` / `contentOrigin` 坏 ⇒ 400;
168
- * `allowTools` / `toolFaces` / `name` / `transport` 坏 ⇒ 200 + 整条 server 不进 + 运维 warn;`elicitation` / `source` / `transport.{args,env,headers,principalHeader}`
169
- * 坏 ⇒ 200 + 悄悄丢键),而「两个键方向相反所以处置不同」只是读器**对存量 body** 的取舍({@link normalizeServer}),不是给在场调用方的答复该有的差别:
170
- * 对一个 fresh 调用方,「收窄声明没生效」与「放宽声明没生效」同样是一份没有生效的声明,响亮拒比两种沉默都好。
152
+ * 请求面 `mcpServers`「这份声明能不能原样用」的**唯一回答者** —— fresh 提交腿({@link assertRequestMcpEntries})与探测口
153
+ * (`capabilities/mcp-probe.ts` 的 `readMcpProbeBody`)都问它、都原样转述它的答案(码与句逐字相同)。`undefined` = 能原样用,或列表缺席 / `null`(没声明;各门自己定缺席的意思)。
154
+ *
155
+ * ## 律:一个回答者、一个拒形,先列表后条目、条目里先键名后键值,第一处坏形即答
156
+ * ① **列表**:在场却不是数组 / 超过 {@link MAX_REQUEST_MCP_SERVERS} 条 / 某一条不是对象 ⇒ `400 request.field_invalid`,点名 `mcpServers` 或 `mcpServers[i]`(句尾同一句,见 {@link LIST_REFUSAL_TAIL})。
157
+ * 整份拒而不是挂前 32 条 / 摘掉读不出的那条 / 整只忽略:一份被悄悄截短、少了几条或整只没生效的声明,在发送方那里读起来是「生效了」。
158
+ * ② **键名**:条目层、或 `transport` 按 `kind` 那一形的键闭集之外的键 ⇒ `400 request.body_shape`,拒体带 `unknownKeys`(拼错的键被悄悄不读,同样读起来像生效了)。
159
+ * ③ **身份头的键组合**(`principalHeader` 与 `assertionHeader` 同设 / `assertionHeader` 非串)⇒ {@link requestEntryIdentityRefusal}(同设那一句对契约包解析期原句逐字)。
160
+ * ④ **受理键的值** ⇒ 逐键问判官表 {@link ENTRY_KEY_JUDGES},`400 request.field_invalid`,拒句点名 `mcpServers[i].<键路径>`、server 名与**该键判官的原因句**({@link entryFieldRefusal})。
161
+ * 判官**就是读器用的那一只**,不是第二份:`toolAxes` = 引擎根导出的 `readMcpToolAxes`;`toolFaces` = `readMcpToolFaces`;`contentOrigin` = `isContentOriginWord`;`source` = `isMcpSourceLabel`;
162
+ * `name` = `isUsableWireName`;`allowTools` / `transport.args` = {@link isStringArray};`transport.env` / `headers` = {@link readPlainStringRecord};`elicitation` = {@link isElicitationFlag};
163
+ * `transport` 的硬字段(`kind` / `command` / `url`)与软字段(`args` / `env` / `headers` / `principalHeader`)= {@link readRequestTransport}(读器投影 transport 的那一只函数,门读它报的 `lost`)。
164
+ * 判官只在引擎里、包根没给读口的键(今天只有 `startupTimeoutMs`)不在这里判,原样过境、由引擎在拨号时按 server 拒(不写第二份判据;到货读口即换)。
165
+ * 于是判官受理的条目,读器({@link normalizeServer})交回的就是申报本身(只差判官自己的合法归一:空容器 / `skillScopeEligible:false` 折成缺席、`toolAxes` 取引擎的拷贝)——
166
+ * 探测口拨的那一份因此就是提交腿会挂的那一份,不另比一遍(另比就是第二只回答者)。唯一按门分的后果是「声明了 `assertionHeader`」这一个事实(引擎尚无座,
167
+ * 承载、不发送):新提交照挂 + 运维 info,探测口整条拒 —— 判据是同一只谓词 `declaresMcpAssertionHeader`,不是第二份判法(见 `readMcpProbeBody`)。
171
168
  *
172
- * ## 每个键一只判官({@link ENTRY_KEY_JUDGES},表的键集两向钉在 `MCP_SERVER_ENTRY_KEYS` 上:新增受理键而没写判官 ⇒ 编译红)
173
- * 判官**就是读器用的那一只**,不是第二份:`toolAxes` = 引擎根导出的 `readMcpToolAxes`;`toolFaces` = `readMcpToolFaces`;`contentOrigin` = `isContentOriginWord`;
174
- * `source` = `isMcpSourceLabel`;`name` = `isUsableWireName`;`allowTools` / `transport.args` = {@link isStringArray};`transport.env` / `headers` = {@link isStringRecord};
175
- * `elicitation` = {@link isElicitationFlag};`transport` 的硬字段(`kind` / `command` / `url`)与软字段(`args` / `env` / `headers` / `principalHeader`)= {@link readRequestTransport}
176
- * (读器投影 transport 的那一只函数,门读它报的 `lost`)。两个身份头的**键组合**(同设 / `assertionHeader` 非串)仍归 `mcp-assertion-header.ts` 的判官({@link requestEntryIdentityRefusal},
177
- * 码 `request.body_shape` 的那一句对契约包原句逐字),在逐键之前先判。
169
+ * ## 射程
170
+ * 纯值域、与租户车道无关(一个写坏的请求在哪条车道上都是写坏的;多租户腿本来就忽略整只数组,告诉调用方「你写错了」只多不少)。
171
+ * 重放存量 body 的腿**不问它**:那条腿走宽容读器(见 {@link assertRequestMcpEntries} 的 leg 分流)。
172
+ * 关于 `contentOrigin` 的信任语义(core 把它定性为**部署面**键):请求腿的条目只有在 {@link mcpInjectionHonored} 为真(单用户 = 请求者就是这台 worker 的超管)时才被读进 `TaskSpec.mcp`,
173
+ * 多租户部署整只数组被忽略 ⇒ 租户**结构上**够不着这个键;名字撞车时 baseline 恒赢({@link mergeRequestMcp})⇒ 调用方也改写不了一台**已配置** server 的声明。
174
+ */
175
+ export declare function requestMcpDeclarationRefusal(list: unknown): RequestMcpRefusal | undefined;
176
+ /**
177
+ * fresh 提交腿的 `mcpServers` 门:{@link requestMcpDeclarationRefusal} 有答案 ⇒ 同步 400(码 / 句 / `unknownKeys` 原样)。
178
178
  *
179
179
  * ## 位置与 leg 分流(沿用本仓成文的 `opts.leg` 姿势,不发明第二套)
180
180
  * 排在腿首、`assertRequestMcpUnlocked` 旁边 —— **任何副作用之前**(①阶段会把附件行持久绑定到 session,②阶段会写 per-session 的 cwd/env 表;一个最终 400 的请求若已经改掉这两样,
181
181
  * 它就改变了后续被接受的任务会物化什么、在哪执行)。**只在 fresh 腿**:`checkpoint_ctx` 存的是**原始** `TaskRequestBody`,耐久 resume 逐字重放它,从这里逃出去的 `HttpError`
182
182
  * 会被 `src/http/server.ts` 折成 `409 resume_blocked_by_policy` ⇒ 一条**升级前** park、body 里带着当时被受理的坏值的任务,升级后永久恢复不了
183
183
  * (本仓三处 `opts.leg` 先例 —— `appendSystemPrompt` 案 / `@model` 名单 [ref] 终裁 (a) / `compactionModel` 案 —— 逐字写着「砖死 resume 比降级糟」)。
184
- * 重放腿走读器({@link normalizeServer}),处置**逐字不变**:坏硬键(`name` / `transport` 硬字段 / `allowTools` / `toolAxes` / `toolFaces`)⇒ 这一条 server 整条不进 + 运维 warn
185
- * `mcp_injection_dropped` + 用户面通告 `mcp.injection_dropped` 点名 `field`(`toolAxes` 在一次性迁移 M19 的窗开着时先按升级前读法重读一次,过了就挂,见读器的注);坏软键(`contentOrigin` / `elicitation` / `source` / `transport` 软字段)⇒ 丢键、server 照挂(`contentOrigin` 另有 `mcp_content_class_dropped` 留声)。
186
- *
187
- * ## 射程
188
- * 判据是**纯值域**、与租户车道无关:一个写坏的请求在哪条车道上都是写坏的,而多租户腿本来就忽略整只数组,告诉调用方「你这个键写错了」只多不少。
189
- * 只判前 `MAX_REQUEST_MCP_SERVERS` 条(能被挂载的那一段,与读器同一个切片)。不在本门的:条目本身不是对象 / `mcpServers` 不是数组(`validateRequestMcp` 的既有宽容形)、词表外的键(读器对未知键宽容,探测口才闭集)、超上限的尾项(`over_cap` 点名;它们不挂载,为一个永远不会生效的声明拒掉整份请求是新开一条拒绝面)。
190
- * 关于 `contentOrigin` 的信任语义(core 把它定性为**部署面**键):请求腿的条目只有在 {@link mcpInjectionHonored} 为真(单用户 = 请求者就是这台 worker 的超管)时才被读进 `TaskSpec.mcp`,
191
- * 多租户部署整只数组被忽略 ⇒ 租户**结构上**够不着这个键;名字撞车时 baseline 恒赢({@link mergeRequestMcp})⇒ 调用方也改写不了一台**已配置** server 的声明。
184
+ * 重放腿走读器({@link normalizeServer} / {@link classifyRequestMcp}),处置:整只不是数组 ⇒ 整只忽略 + 运维 warn `mcp_injection_ignored`;条目不是对象 / 坏硬键
185
+ * (`name` / `transport` 硬字段 / `allowTools` / `toolAxes` / `toolFaces`)⇒ 这一条 server 整条不进 + 运维 warn `mcp_injection_dropped` + 用户面通告 `mcp.injection_dropped` 点名 `field`
186
+ * (`toolAxes` 在一次性迁移 M19 的窗开着时先按升级前读法重读一次,过了就挂,见读器的注);超上限的尾项 ⇒ `over_cap` 点名;坏软键(`contentOrigin` / `elicitation` / `source`)
187
+ * 与词表外的键 ⇒ 丢键、server 照挂(`contentOrigin` 另有 `mcp_content_class_dropped` 留声);`transport` 软字段 ⇒ 只不交坏的那几处(`env` / `headers` 对 JSON 存体按 7.131.x 的判据整键交或不交,
188
+ * 交出去的剔掉 `__proto__` 名成员),server 照挂,运维 warn `mcp_transport_members_dropped` 逐处点名 —— 续跑重放腿不静默丢声明。
192
189
  */
193
190
  export declare function assertRequestMcpEntries(bodyMcp: unknown): void;
194
- /**
195
- * 一条**原始**请求条目上 `toolAxes` 的拒面(码 + 句子);没有问题 / 条目不是对象 / 没写 `toolAxes` ⇒ `undefined`。探测口(`mcp-probe.ts` 的 `entryValueIssue`)在它自己的通用读器之前读这一只,
196
- * 所以 `toolAxes` 在两扇拒绝门上是**同一句**:判官 = {@link ENTRY_KEY_JUDGES} 的 `toolAxes` 格(引擎的 `readMcpToolAxes`),句子 = {@link entryFieldRefusal}(与 fresh 提交腿的每个键同一个形)。
197
- * `at` = 条目的键路径(`mcpServers[3]`)。
198
- */
199
- export declare function requestEntryToolAxesRefusal(entry: unknown, at: string): {
200
- errorCode: "request.field_invalid";
201
- message: string;
202
- } | undefined;
203
191
  /**
204
192
  * The ONE predicate for "this MCP server name belongs to the deployment": the scenario baseline (what mounted for
205
193
  * this scenario) ∪ every name the deployment DECLARED (`CenterMcp.reservedNames` — mounted or not, any scenario).