@sema-agent/server 7.135.0 → 7.136.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/MIGRATION.md +64 -2
  2. package/USAGE.md +62 -25
  3. package/deploy/sema-up/README.md +1 -1
  4. package/deploy/sema-up/chart/templates/NOTES.txt +1 -1
  5. package/deploy/sema-up/chart/values.yaml +1 -1
  6. package/dist/approval-card.d.ts +60 -4
  7. package/dist/approval-card.js +18 -1
  8. package/dist/approval-content-kind.d.ts +1 -0
  9. package/dist/approval-content-kind.js +1 -1
  10. package/dist/auth-bridge.d.ts +34 -5
  11. package/dist/auth-bridge.js +20 -3
  12. package/dist/boot/budget-tracing.d.ts +1 -2
  13. package/dist/boot/budget-tracing.js +2 -2
  14. package/dist/boot/org-memory.js +1 -1
  15. package/dist/boot/resolve-spec.js +10 -7
  16. package/dist/boot/runner-deps.js +4 -2
  17. package/dist/boot/stage-04-stores.d.ts +1 -1
  18. package/dist/boot/stage-05-execution-env.d.ts +1 -1
  19. package/dist/boot/stage-06-runners.d.ts +1 -1
  20. package/dist/boot/stage-07-capability-layer.d.ts +1 -1
  21. package/dist/boot/stage-08-reapers.d.ts +1 -1
  22. package/dist/boot/stage-09-leader.d.ts +1 -1
  23. package/dist/boot/stage-10-http-server.d.ts +1 -1
  24. package/dist/boot/stores.js +2 -3
  25. package/dist/boot/web-admin.js +10 -17
  26. package/dist/brain.js +12 -3
  27. package/dist/budget.d.ts +3 -3
  28. package/dist/budget.js +4 -0
  29. package/dist/capabilities/mcp-probe.d.ts +5 -4
  30. package/dist/capability-facts.d.ts +3 -7
  31. package/dist/capability-facts.js +3 -5
  32. package/dist/center-credential.d.ts +18 -4
  33. package/dist/center-credential.js +18 -3
  34. package/dist/config-catalog.js +12 -10
  35. package/dist/config-d-migration.js +1 -1
  36. package/dist/config-invariants.d.ts +1 -1
  37. package/dist/config-invariants.js +8 -0
  38. package/dist/config-lkg.d.ts +0 -38
  39. package/dist/config-lkg.js +1 -83
  40. package/dist/config-types.d.ts +12 -4
  41. package/dist/config.js +26 -8
  42. package/dist/deployment-shape.d.ts +1 -1
  43. package/dist/elicitation.d.ts +8 -7
  44. package/dist/elicitation.js +9 -8
  45. package/dist/file-history-disclosure.d.ts +6 -4
  46. package/dist/file-history-disclosure.js +22 -6
  47. package/dist/fleet-client.d.ts +32 -1
  48. package/dist/fleet-client.js +24 -4
  49. package/dist/fleet-lease.d.ts +7 -3
  50. package/dist/fleet-lease.js +31 -23
  51. package/dist/hooks/hook-llm.js +14 -8
  52. package/dist/hooks/hook-runner.d.ts +4 -4
  53. package/dist/hooks/hook-runner.js +2 -2
  54. package/dist/http/checkpoint-ctx.d.ts +28 -9
  55. package/dist/http/checkpoint-ctx.js +5 -1
  56. package/dist/http/dispatch.js +9 -9
  57. package/dist/http/resume-ctx-read.d.ts +2 -16
  58. package/dist/http/resume-ctx-read.js +2 -3
  59. package/dist/http/routes/approvals-assistant.d.ts +0 -2
  60. package/dist/http/routes/approvals-assistant.js +47 -12
  61. package/dist/http/routes/runs.js +2 -2
  62. package/dist/http/routes/session-sync.js +31 -9
  63. package/dist/http/routes/sessions.js +18 -14
  64. package/dist/http/submit-ctx.js +1 -1
  65. package/dist/http/wire-types.d.ts +5 -3
  66. package/dist/index.d.ts +1 -0
  67. package/dist/index.js +1 -0
  68. package/dist/model-route-gap.d.ts +49 -4
  69. package/dist/model-route-gap.js +34 -1
  70. package/dist/object-store-buckets.d.ts +13 -0
  71. package/dist/object-store-buckets.js +3 -0
  72. package/dist/observability/fail-open.d.ts +6 -6
  73. package/dist/observability/fail-open.js +6 -6
  74. package/dist/observability/metrics.js +1 -1
  75. package/dist/observability/principal-context.d.ts +4 -1
  76. package/dist/observability/principal-context.js +6 -1
  77. package/dist/parked-decide.d.ts +4 -3
  78. package/dist/parked-decide.js +26 -7
  79. package/dist/peer-directory.d.ts +2 -1
  80. package/dist/peer-directory.js +5 -5
  81. package/dist/plugins/approval-ask-store-sql.d.ts +8 -0
  82. package/dist/plugins/approval-ask-store-sql.js +3 -0
  83. package/dist/plugins/blob-backend.js +2 -1
  84. package/dist/plugins/checkpoint-index-columns.d.ts +50 -1
  85. package/dist/plugins/checkpoint-index-columns.js +28 -0
  86. package/dist/plugins/checkpoint-store-sql.d.ts +27 -3
  87. package/dist/plugins/checkpoint-store-sql.js +58 -29
  88. package/dist/plugins/file-history-store-sql.d.ts +6 -0
  89. package/dist/plugins/file-history-store-sql.js +30 -23
  90. package/dist/plugins/local-checkpoint-store.js +4 -3
  91. package/dist/plugins/one-time-migrations.d.ts +21 -28
  92. package/dist/plugins/one-time-migrations.js +39 -123
  93. package/dist/plugins/remote-env-k8s.d.ts +18 -5
  94. package/dist/plugins/remote-env-k8s.js +24 -7
  95. package/dist/plugins/s3-answer.d.ts +15 -0
  96. package/dist/plugins/s3-answer.js +5 -0
  97. package/dist/plugins/snapshot-bucket-presence.d.ts +39 -4
  98. package/dist/plugins/snapshot-bucket-presence.js +35 -7
  99. package/dist/plugins/sql-errors.d.ts +39 -0
  100. package/dist/plugins/sql-errors.js +49 -1
  101. package/dist/plugins/store-fault.d.ts +4 -0
  102. package/dist/plugins/store-fault.js +8 -0
  103. package/dist/plugins/tidb-pool.js +1 -1
  104. package/dist/plugins/web-search.d.ts +1 -1
  105. package/dist/plugins/web-search.js +2 -2
  106. package/dist/request-declaration-refusal.d.ts +94 -0
  107. package/dist/request-declaration-refusal.js +78 -0
  108. package/dist/request-image-urls.d.ts +6 -0
  109. package/dist/request-image-urls.js +27 -0
  110. package/dist/request-key-closure.d.ts +5 -0
  111. package/dist/request-key-closure.js +4 -1
  112. package/dist/runtime-caps-resolver.js +1 -1
  113. package/dist/sandbox-pkg-source.d.ts +28 -1
  114. package/dist/sandbox-pkg-source.js +1 -2
  115. package/dist/sealed-key.d.ts +19 -0
  116. package/dist/sealed-key.js +47 -4
  117. package/dist/session-incarnation.d.ts +16 -5
  118. package/dist/session-incarnation.js +7 -4
  119. package/dist/session-row.d.ts +3 -17
  120. package/dist/session-row.js +4 -0
  121. package/dist/session-sync.d.ts +15 -3
  122. package/dist/session-sync.js +5 -2
  123. package/dist/task-a2a.d.ts +52 -81
  124. package/dist/task-a2a.js +219 -89
  125. package/dist/task-mcp.d.ts +6 -14
  126. package/dist/task-mcp.js +16 -51
  127. package/dist/tool-approval.d.ts +23 -10
  128. package/dist/tool-approval.js +51 -36
  129. package/dist/trace/core-keyset-guard.d.ts +2 -2
  130. package/dist/trace/exit-seat.js +37 -13
  131. package/dist/trace/redact.d.ts +28 -12
  132. package/dist/trace/redact.js +233 -94
  133. package/dist/trace/self-minted-wire.d.ts +267 -0
  134. package/dist/trace/self-minted-wire.js +79 -0
  135. package/dist/url-credential-refusal.d.ts +52 -0
  136. package/dist/url-credential-refusal.js +14 -0
  137. package/dist/url-refusal.d.ts +13 -4
  138. package/dist/url-refusal.js +8 -2
  139. package/dist/workspace-space.d.ts +27 -0
  140. package/dist/workspace-space.js +26 -0
  141. package/package.json +3 -2
  142. package/scripts/drain-check-local.mjs +2 -1
  143. package/scripts/drain-check.mysql.sql +3 -1
  144. package/scripts/drain-check.pg.sql +3 -1
  145. package/skills/schedule.md +1 -1
  146. package/wire-changes/server@7.104.0.json +134 -0
  147. package/wire-changes/server@7.105.0.json +53 -0
  148. package/wire-changes/server@7.106.0.json +475 -0
  149. package/wire-changes/server@7.107.0.json +64 -0
  150. package/wire-changes/server@7.108.0.json +153 -0
  151. package/wire-changes/server@7.109.0.json +46 -0
  152. package/wire-changes/server@7.110.0.json +90 -0
  153. package/wire-changes/server@7.111.0.json +86 -0
  154. package/wire-changes/server@7.112.0.json +60 -0
  155. package/wire-changes/server@7.113.0.json +26 -0
  156. package/wire-changes/server@7.114.0.json +26 -0
  157. package/wire-changes/server@7.115.0.json +97 -0
  158. package/wire-changes/server@7.116.0.json +218 -0
  159. package/wire-changes/server@7.116.1.json +26 -0
  160. package/wire-changes/server@7.117.0.json +65 -0
  161. package/wire-changes/server@7.118.0.json +129 -0
  162. package/wire-changes/server@7.119.0.json +65 -0
  163. package/wire-changes/server@7.120.0.json +89 -0
  164. package/wire-changes/server@7.121.0.json +219 -0
  165. package/wire-changes/server@7.122.0.json +54 -0
  166. package/wire-changes/server@7.123.0.json +26 -0
  167. package/wire-changes/server@7.124.0.json +336 -0
  168. package/wire-changes/server@7.124.1.json +26 -0
  169. package/wire-changes/server@7.125.0.json +111 -0
  170. package/wire-changes/server@7.126.0.json +285 -0
  171. package/wire-changes/server@7.127.0.json +77 -0
  172. package/wire-changes/server@7.127.1.json +26 -0
  173. package/wire-changes/server@7.127.2.json +53 -0
  174. package/wire-changes/server@7.128.0.json +26 -0
  175. package/wire-changes/server@7.129.0.json +106 -0
  176. package/wire-changes/server@7.129.1.json +26 -0
  177. package/wire-changes/server@7.130.0.json +26 -0
  178. package/wire-changes/server@7.131.0.json +118 -0
  179. package/wire-changes/server@7.132.0.json +226 -0
  180. package/wire-changes/server@7.133.0.json +1155 -0
  181. package/wire-changes/server@7.134.0.json +107 -0
  182. package/wire-changes/server@7.135.0.json +174 -0
  183. package/wire-changes/server@7.136.0.json +448 -0
  184. package/dist/url-credential.d.ts +0 -52
  185. package/dist/url-credential.js +0 -49
@@ -27,8 +27,8 @@
27
27
  * · **一位活住客必须有令牌 —— 读到空化身 ⇒ 响亮拒**({@link SessionIncarnationMissingError}):本仓比 core 多的那一条律,只住在一处 ——
28
28
  * 把头投成 {@link SessionFace}(属主 + 一只令牌)的 {@link sessionFaceOfHead}(批量来源读与换装回读用同一只 {@link sessionFaceFromRow})。
29
29
  * 要拿令牌去**写**(带化身条件的 UPDATE、认领、空间分区键)或要响亮区分「行坏了」的读者经它;只**比较**两头的读者用 `compareOccupants`。
30
- * 一次性迁移(`plugins/one-time-migrations.ts` 登记表 M5)之后,空值只可能来自没铸化身的写腿或人为改库 —— 把它当「通配」就是让任何人
31
- * 都能和这一行比成同一位住客。
30
+ * 空值只可能来自没铸化身的写腿、人为改库,或跳过了 7.104.0–7.135.x(那几版的起服迁移给存量行铸令牌,窗关后已删码)直接升上来的库 ——
31
+ * 把它当「通配」就是让任何人都能和这一行比成同一位住客。
32
32
  * · **不上 wire、不进模型上下文、不进日志正文**:日志里要点名时只打 {@link incarnationLogPrefix}(前 8 位)。
33
33
  *
34
34
  * ## 谁读它(全部派生自那一只头读)
@@ -97,8 +97,19 @@ export declare class UnreadableLeaf {
97
97
  * 与 core 的读法同义 —— 拒不拒是投影的事,见 {@link sessionFaceOfHead})。叶子那一格读不出({@link UnreadableLeaf})⇒ 头照答属主与住客,
98
98
  * 读 `leafId` 的那一位收到那只故障 —— 永不把读不出的叶子答成 `null`(`null` = 空会话)。 */
99
99
  export declare function headFromRow(sessionId: string, owner: string | null, incarnation: unknown, leaf: string | null | UnreadableLeaf): SessionHead;
100
+ /**
101
+ * 🔴 **会话内容读不读得出 —— 会话路由在门放行之后判它的唯一读法**:读头的叶子。内容读不出的头({@link UnreadableLeaf})在这一读上抛那只故障本身
102
+ * (码 `corrupt_session`,经 `sessionErrorHttp` 的 open 面落 500);读得出 ⇒ 叶子 id(`null` = 空会话)。
103
+ * 谁在**门放行之后**要动这只会话的内容(读叶子本身、开事件流、改名 —— 改名答的是列表行,列表读存档),谁就经它读一次;
104
+ * 门之前不许读(非属主对坏档与对好档同答,见头注「头答自会话行」)。三个口各写一句 `head.leafId` 与各写一句"存档坏了怎么办"是同一件事的三种答法
105
+ * (修前:头探针 500、事件流 200 零帧 —— 观察探针把每拍读叶子抛出的故障当瞬时错吞掉 —— 改名 404 —— 列表跳过读不出的存档,会话被说成不存在)。
106
+ * SQL 两店的叶子是行上的一列,永不抛 ⇒ 这一读在 SQL 上恒等于 `head.leafId`,这条判据只对内容与行分居两处的店(local)有牙。
107
+ * 事件流只在开流之前判一次;流开了之后存档才坏,观察探针仍把读叶子的故障当瞬时错续探(流不断、不再产出新帧)。
108
+ */
109
+ export declare function leafOfAdmittedHead(head: SessionHead): string | null;
100
110
  /** 一只头 → 一位活住客({@link SessionFace}):`undefined` 头 = 没有这只会话 ⇒ `undefined`;行在而没有令牌 ⇒ {@link SessionIncarnationMissingError}
101
111
  * (本仓的那条律只住在这里 + {@link sessionFaceFromRow})。 */
112
+ export declare function sessionFaceOfHead(sessionId: string, head: SessionHead): SessionFace;
102
113
  export declare function sessionFaceOfHead(sessionId: string, head: SessionHead | undefined): SessionFace | undefined;
103
114
  /**
104
115
  * 「这只会话店有没有按 id 读(`headOf`)」—— 全仓**只判这一处**:授权口、各路由的属主门、能力位、起服门、k8s 工作区空间的每 run 读与
@@ -116,12 +127,12 @@ export declare function carriedIncarnation(sessionId: string, prior: {
116
127
  } | undefined, owner: string | null): string;
117
128
  /**
118
129
  * `session_meta.incarnation` 的列定义 —— 两方言各一句,**唯一属主**:中央 DDL(`tidb-pool.ts` / `pg-session-storage.ts` 的
119
- * `CREATE TABLE session_meta`)与一次性迁移 M5 的 `ADD COLUMN`(`plugins/one-time-migrations.ts`)都内插这同一串,
120
- * 于是「新库建出来的列」与「存量库补上的列」逐字同形([ref] 起服列册探针比的是列名,形由这里保证)。
130
+ * `CREATE TABLE session_meta`)内插这同一串;存量库缺这一列时,起服列册探针(`schema-consistency.ts`)拒启,拒句里的 `ALTER TABLE … ADD COLUMN …`
131
+ * 由同一份建表语句解析生成 ⇒ 「新库建出来的列」与「运维按拒句补上的列」逐字同形(探针比的是列名,形由这里保证)。
121
132
  *
122
133
  * `NOT NULL DEFAULT ''`:空串就是「没铸过」的唯一表示 —— 读口对它响亮拒({@link SessionIncarnationMissingError})。
123
134
  * 缺省值只为两件事存在:①存量表 `ADD COLUMN` 在两方言上一句成立(PG 对非空表加无缺省的 NOT NULL 列直接拒);
124
- * ②一条漏铸的写腿落下的是**读得出来的错**(读口拒),而不是一条插入期的 500 —— 两形都 fail-closed,前者还能被迁移补上。
135
+ * ②一条漏铸的写腿落下的是**读得出来的错**(读口拒),而不是一条插入期的 500 —— 两形都 fail-closed。
125
136
  * 宽度 64:`randomUUID()` 是 36 字符,留出余量但不给任意长串留位。
126
137
  */
127
138
  export declare const SESSION_META_INCARNATION_COLUMN: {
@@ -13,10 +13,10 @@ export class SessionIncarnationMissingError extends Error {
13
13
  sessionId;
14
14
  code = "session.incarnation_missing";
15
15
  constructor(sessionId) {
16
- super(`session ${sessionId} has no incarnation token: every session row is minted one when it is created, and rows written before ` +
17
- `7.104.0 get one from the one-time migration that runs at boot (plugins/one-time-migrations.ts, entry M5). An empty value ` +
18
- `after that means a write path that did not mint one, or a hand-edited row — it is refused rather than read as "matches anyone". ` +
19
- `Restart a 7.104.0+ replica so the migration backfills it, or delete the session.`);
16
+ super(`session ${sessionId} has no incarnation token: every session row is minted one when it is created. An empty value means a write path ` +
17
+ `that did not mint one, a hand-edited row, or a database upgraded from before 7.104.0 straight to a version after 7.135.x (a boot of any ` +
18
+ `version in between minted tokens for the older rows; the code that did so has since been removed) — it is refused rather than read as ` +
19
+ `"matches anyone". Give the row a distinct random token (any opaque string of at most 64 characters), or delete the session.`);
20
20
  this.sessionId = sessionId;
21
21
  this.name = "SessionIncarnationMissingError";
22
22
  }
@@ -57,6 +57,9 @@ export function headFromRow(sessionId, owner, incarnation, leaf) {
57
57
  },
58
58
  };
59
59
  }
60
+ export function leafOfAdmittedHead(head) {
61
+ return head.leafId;
62
+ }
60
63
  export function sessionFaceOfHead(sessionId, head) {
61
64
  return head === undefined ? undefined : sessionFaceFromRow(sessionId, head.owner, head.incarnation);
62
65
  }
@@ -1,19 +1,3 @@
1
- /**
2
- * 会话行的**可写位**(`title` / `pinned`)—— `PATCH /v1/sessions/:id` 的体闭集、两键的判据,与 2c 导入同读的那一套(唯一属主)。
3
- *
4
- * ## 一列一事实,写者次序成文
5
- * · `title` 是一列(SQL `session_meta.title` / local 标题边档),两个写者,一条律:**用户**经会话店唯一写口 `patchSessionMeta`
6
- * **无条件**写 —— 写一个串 = 改名,写 `null` = **清回自动标题**(列回到 NULL);**自动标题器**经 `setTitleIfNull` **只填 NULL**
7
- * (每次提交按列读一次:列是 NULL 才起名,见 `session-titler.ts`)。所以清与填是同一列同一律:用户清掉之后,下一次提交由自动标题器
8
- * 照常填;用户写过串的会话,自动标题器永不覆盖(无条件写盖得掉自动标题,NULL 守卫盖不掉用户标题)。没有第二列(不另设 `userTitle` /
9
- * `label`)。清的拼法只有 `null` 一种 —— 空串 `""` 是坏体(`request.body_shape`),不是「清」。
10
- * · `pinned` 是一列布尔,缺省 `false`;列表行**恒在场**(置顶是会话的状态,不是能力)。
11
- *
12
- * ## 上界 = 列宽
13
- * {@link SESSION_TITLE_MAX_CHARS} 是 `title` 列的宽度,两方言 DDL 内插的就是这一个数(`tidb-pool.ts` / `pg-session-storage.ts`),
14
- * wire 上界与存储上界是同一个事实。按**字符**计(Unicode 码点 —— 与 MySQL 协议 utf8mb4 / PostgreSQL 的 `VARCHAR(n)` 同一单位),
15
- * 不按 UTF-16 位计。自动标题器自己的显示预算(`session-titler.ts` 的 `TITLE_MAX_CHARS`)更短,落在这个上界之内。
16
- */
17
1
  /** `title` 列的宽度(字符)= 用户标题的上界。两方言 DDL 内插它。 */
18
2
  export declare const SESSION_TITLE_MAX_CHARS = 120;
19
3
  /** 会话行的可写位(体闭集;两键至少一键)。会话店唯一写口 `patchSessionMeta` 的形参。`title: null` = 清回自动标题(列写 NULL)。 */
@@ -28,7 +12,9 @@ export interface SessionRowProblem {
28
12
  }
29
13
  /**
30
14
  * `title` 一格的**唯一**读者(PATCH 体与 2c 导入同读)。`null` ⇒ 清回自动标题;串 ⇒ 1..{@link SESSION_TITLE_MAX_CHARS} 个字符、首尾无空白、
31
- * 不含 C0 控制字符({@link C0_CONTROL}),存原文(不脱敏、不改写);空串 ⇒ `request.body_shape`(清的拼法只有 `null`);其余 ⇒ `request.field_invalid`。
15
+ * 不含 C0 控制字符({@link C0_CONTROL})、至少有一个可见字符(经 core 的 `normalizePeerName` 不为空 —— NFKC → 去控制 / 格式码点 → trim;
16
+ * 与跨会话对端目录判「这一行有没有可用名」是**同一只**判据,`peer-directory.ts`),存原文(不脱敏、不改写);空串 ⇒ `request.body_shape`(清的拼法只有 `null`);其余 ⇒ `request.field_invalid`。
17
+ * 三道串判的先后固定:边空白 → C0 → 无可见字符(带 C0 的串先报 C0)。
32
18
  */
33
19
  export declare function readSessionTitle(v: unknown): {
34
20
  readonly title: string | null;
@@ -1,7 +1,9 @@
1
+ import { normalizePeerName } from "@sema-agent/core";
1
2
  export const SESSION_TITLE_MAX_CHARS = 120;
2
3
  const SESSION_ROW_PATCH_KEYS = { title: true, pinned: true };
3
4
  const BODY_SHAPE_MESSAGE = "the body must be a JSON object carrying at least one of `title` and `pinned`, and nothing else";
4
5
  const EMPTY_TITLE_MESSAGE = "title must not be the empty string — send `null` to clear it back to the automatic title";
6
+ const INVISIBLE_TITLE_MESSAGE = "title must contain at least one visible character";
5
7
  const C0_CONTROL = /[\u0000-\u001f]/u;
6
8
  export function readSessionTitle(v) {
7
9
  if (v === null)
@@ -16,6 +18,8 @@ export function readSessionTitle(v) {
16
18
  return { problem: { code: "request.field_invalid", message: "title must not start or end with whitespace" } };
17
19
  if (C0_CONTROL.test(v))
18
20
  return { problem: { code: "request.field_invalid", message: "title must not contain control characters (U+0000 to U+001F)" } };
21
+ if (normalizePeerName(v) === "")
22
+ return { problem: { code: "request.field_invalid", message: INVISIBLE_TITLE_MESSAGE } };
19
23
  return { title: v };
20
24
  }
21
25
  export function readSessionPinned(v) {
@@ -50,7 +50,7 @@ import type { ServiceFileHistoryStore, ServiceSessionPolicyStore, ResumeAnchorSt
50
50
  /**
51
51
  * core 7.55.0 [ref] T2([ref]):一只会话的文件历史**读不全**(撕裂 / 读不出的边界、读不出的跟踪集 —— 店的 `exportHistory` 答 `{ ok: false }`,`adopt_failed`
52
52
  * 带店判的 `permanence`)。它**不是**「没有历史」:读成 `null` 的同步会把这只会话的全部 rewind 历史丢在路上、还答成功。会话同步的两只拉取口
53
- * (manifest、history blob)对它答同一句 503 `state.file_history_unreadable`(路由 `sendFileHistoryUnreadable`)。
53
+ * (manifest、history blob)对它答同一句 503 `state.file_history_unreadable`(路由 `sendPullUnreadable`;连接类故障在拉取口同码同体键答 `permanence: "transient"`)。
54
54
  */
55
55
  export declare class FileHistoryUnreadableError extends Error {
56
56
  readonly sessionId: string;
@@ -67,9 +67,21 @@ export declare function exportedFileHistory(store: Pick<ServiceFileHistoryStore,
67
67
  * Returned by {@link exportSessionManifest} (closes over the source store); consumed wherever a caller pulls
68
68
  * content-addressed history bytes lazily (the live PUSH route's `importHistory` replay). */
69
69
  export type BlobGetter = (hash: string) => Promise<Uint8Array | undefined>;
70
- /** 一只文件历史店自己的 blob 取值口({@link BlobGetter}):店带 `getBlob` ⇒ 绑在店上;没有这条面(core 的 local 文件店)⇒ 恒答 `undefined`(「这里取不到」)。
70
+ /** blob 两面(`putBlob` / `getBlob`)齐备的文件历史店(marker 门,不做名字探测:local 的引擎文件店带同名私有助手)。 */
71
+ export type SyncBlobHistoryStore = ServiceFileHistoryStore & {
72
+ readonly syncBlobFaces: true;
73
+ } & {
74
+ [P in "putBlob" | "getBlob"]-?: NonNullable<ServiceFileHistoryStore[P]>;
75
+ };
76
+ /**
77
+ * 「这只文件历史店有没有 blob 两面」的**唯一**读法:marker `syncBlobFaces`(只有 SQL 两只店立它)∧ 两面在场。能力事实 `sessionSyncFileHistory`(capability-facts)
78
+ * 与取值口 {@link blobGetterOf} 都读它 —— 事实说有,取值口就通到事实店的 `getBlob`;事实说没有,取值口恒空。
79
+ * 名字探测(`store.getBlob?.…`)不可靠:core 的 local 文件店带与服务面同名的**私有**助手(同形 `putBlob` 已被 marker 挡过一次),名字探会错拿到它。
80
+ */
81
+ export declare function syncBlobFacesOf(store: ServiceFileHistoryStore): SyncBlobHistoryStore | undefined;
82
+ /** 一只文件历史店自己的 blob 取值口({@link BlobGetter}):从 {@link syncBlobFacesOf} 派生 —— 有 blob 两面 ⇒ 绑在店上的 `getBlob`;没有(core 的 local 文件店)⇒ 恒答 `undefined`(「这里取不到」)。
71
83
  * 拉取面(manifest 的 `getBlob`)、identical 自愈臂、提交臂三处读同一只,不各写一份「无面则 undefined」。 */
72
- export declare function blobGetterOf(store: Pick<ServiceFileHistoryStore, "getBlob">): BlobGetter;
84
+ export declare function blobGetterOf(store: ServiceFileHistoryStore): BlobGetter;
73
85
  /** The history-import capability face of a `FileHistoryStore` (`importHistory` is OPTIONAL on the core
74
86
  * contract — a store without it cannot be a 2c history sink). */
75
87
  export interface FileHistoryImportFace {
@@ -21,9 +21,12 @@ export async function exportedFileHistory(store, sessionId) {
21
21
  throw new FileHistoryUnreadableError(sessionId, exported.error);
22
22
  return exported.history;
23
23
  }
24
+ export function syncBlobFacesOf(store) {
25
+ return store.syncBlobFaces === true && typeof store.putBlob === "function" && typeof store.getBlob === "function" ? store : undefined;
26
+ }
24
27
  export function blobGetterOf(store) {
25
- const get = store.getBlob?.bind(store);
26
- return get ? (hash) => get(hash) : async () => undefined;
28
+ const faces = syncBlobFacesOf(store);
29
+ return faces ? (hash) => faces.getBlob(hash) : async () => undefined;
27
30
  }
28
31
  export function fileHistoryImportFace(fs) {
29
32
  const candidate = fs?.importHistory;
@@ -1,102 +1,73 @@
1
- /**
2
- * [DESIGN-269 车1 §2.2/§2.3 — A2A client seam] — project a caller's per-request A2A peers
3
- * (`TaskRequest.a2aPeers`) into the engine, so a `sema` run can talk to the agents the USER declares,
4
- * not only the ones the deployment's web-admin declares. Point-for-point mirror of `task-mcp.ts`
5
- * (the MCP seam), because the trust question is the same question — and deliberately NOT a copy where
6
- * the two protocols differ (see 「A2A 比 MCP 窄」 below).
7
- *
8
- * 🔒 SECURITY — an A2A peer is a remote **AGENT**, not a data source: it acts on its own side. core ruled
9
- * (2026-08-03, `A2aServerSpec` doc) that every skill such a peer advertises mounts with `egress: true` +
10
- * `effect: "write"`, i.e.接入即引入对外写通道 — it registers the approval gate, is never auto-allowed on a
11
- * no-policy deployment, and its replies reach the model inside an untrusted-data fence. The trust boundary
12
- * is therefore the SAME one the MCP seam draws, for the same reason:
13
- * - SINGLE-USER deployment (`requirePrincipal !== false`… i.e. `!== true`): the requester IS the
14
- * super-admin of their own worker, so the peers they name are their own choice (CC-parity), on EVERY
15
- * execution lane — an A2A call is a plain outbound HTTPS request made by the worker, so the lane where
16
- * the agent's TOOL CALLS run is irrelevant (exactly the fact that decoupled the MCP gate from the lane).
17
- * - MULTI-TENANT deployment (`requirePrincipal === true`): a tenant must NOT make the SHARED worker POST
18
- * to a body-chosen URL (SSRF) carrying body-chosen headers, nor mount write-capable tools from an agent
19
- * the operator never vetted. Gate CLOSED → body peers ignored; the fleet gets peers only through center
20
- * config (`config.a2aPeers`).
21
- * When the gate is closed a sent `a2aPeers` is IGNORED — and SAID (`a2a_injection_ignored` + the
22
- * `capabilities.a2aInjection` advertisement), never silently swallowed.
23
- *
24
- * ⚠️ **A2A 的轴比 MCP 窄,这不是简化而是协议事实**:A2A has no per-skill annotation vocabulary, so there is
25
- * no server-hint leg to fold — `toolAxes` (the CALLER's own judgement) is the ONLY thing that can move an axis
26
- * off the fail-closed default. core says as much on `A2aToolAxis`: `irreversibility:"never"` / `egress:false`
27
- * 「appears ONLY via an explicit caller override」, enforcement ignores the loosening (tighten-only) and the
28
- * ask's risk-axes REPORT face consumes it. So this module carries the caller's overrides through verbatim —
29
- * they are a judgement, not silence — while knowing that core will not let them widen enforcement.
30
- *
31
- * ⚠️ **两票判据的词表在 core 5.36 尚未到货**(分单件 C-1/C-2,core 已认领排 5.37 后首个 A2A 窗):
32
- * `LockedKey` 是 `"mcp"|"toolPolicy"|"compliancePosture"|"retentionPolicy"`,`ComplianceCapability` 是
33
- * `"mcp_servers"|"workflows"|"web_fetch"|"org_memory_mount"` —— 两张表里都还没有 A2A 的位。本模块因此按
34
- * **字符串**判(server config 的 `lockedConfigKeys` 本就是 server 自己的声明面,不经 core 闭集型),
35
- * 判据一到货就换 core 真源、钉子逐字不用改。
36
- * 🔴 **今天这两票在 env 通道上还打不响**,而这是刻意如实记下的、不是被忽略的缺口:`LOCKED_CONFIG_KEYS`
37
- * 经 core 的 `resolveLockedKeys` 校验(未知键拒启)、`COMPLIANCE_ADDITIONAL_DENIES` 经 `COMPLIANCE_CAPABILITIES`
38
- * 校验(同拒),所以运维今天**写不进** `a2a` / `a2a_peers`。而且合规那一票还有第二层:即便有人跳过 boot 期
39
- * 校验把 `a2a_peers` 塞进档位,`resolveComplianceDenies` 对闭集外的词是**抛**(亲测 core 5.36),于是这条
40
- * 请求腿的失败方向是**响亮**(500)而不是安静放行 —— 方向正确,故本模块不加任何兜底。两票的代码先在场是
41
- * 纵深:core 词表到货那一拍这条腿自己就活了,不需要有人记得回来补;反过来(等词表到了再写判据)才是
42
- * 「广告了却拦不住」的那一类缺口。钉在 `test/task-a2a.test.ts`(含到货后该怎么改那一格的原地说明)。
43
- */
44
- import { type A2aServerSpec, type CompliancePosture } from "@sema-agent/core";
1
+ import type { A2aServerSpec } from "@sema-agent/core";
2
+ import { type DeclarationRefusal } from "./request-declaration-refusal.js";
45
3
  import type { StoredToolAxesRereadM19 } from "./stored-request-tool-axes-m19.js";
4
+ import { type PrincipalAxisInput } from "./deployment-shape.js";
46
5
  /** Bound on caller-supplied peers (anti-DoS; a real declaration names a handful of agents). */
47
6
  export declare const MAX_REQUEST_A2A_PEERS = 32;
48
- /** The deployment-facing shape this module reads. `lockedConfigKeys` is deliberately `readonly string[]`
49
- * and not core's `LockedKey[]`: the `a2a` member does not exist in core's union yet (see module header),
50
- * and a predicate that cannot even NAME the key it guards is a predicate that silently guards nothing. */
51
- export interface A2aGateConfig {
52
- requirePrincipal?: boolean;
53
- lockedConfigKeys?: readonly string[];
54
- compliancePosture?: CompliancePosture;
7
+ /**
8
+ * 一条**没有被挂上**的请求腿对端的事实 —— 运维 warn `a2a_injection_dropped` 的 `dropped` 条目(`validateRequestA2a` 的 `dropped` 同形)。与 MCP 面 {@link McpInjectionDrop}(`mcp_injection_dropped`)
9
+ * **同形同键名**:`reason` · `field`,只有名字键各座自己的(MCP `server` ↔ A2A `peer`;键集两向钉在下面的 `_GuardDropKeys*` 上,任一边改名 ⇒ 编译红)。
10
+ * · `peer`:申报名(调用方文本,限长 128,读不出 ⇒ `<malformed>`;撞帽的尾项是一条合并标签 `(+N over cap of 32)`);
11
+ * · `reason`:**判官表**({@link A2A_PEER_KEY_JUDGES})对这一键的原因句 —— 与同一份体上新提交门 400 拒句里的那一句逐字同一句(同一只组句器 `declarationIssueFacts`,不回显值),**只有一臂例外**:
12
+ * 一次性迁移 M19 的重读入参克隆不了(访问器成员 / BigInt / Proxy / 环 / 非纯数据)⇒ 句子带前缀 `declaration not cloneable (<because>) — the one-time migration re-reads the stored declaration as plain JSON data and this one is not; `,**其后**才是判官对原声明的那一句(新提交门不铸这一臂,所以 400 拒句里没有这个前缀);
13
+ * 条目不是对象 = 列表级骨架的头一句(`declarationEntryNotObject`);撞帽 = 撞帽铸点的头一句(`declarationOverCapFacts`,受理口撞帽拒句以同一句开头)。**`reason` 只有一种内容 = 句子**,本座不自铸原因词,
14
+ * 机读词(`over_cap`)留在 400 拒体的 `facts`、不进日志的 `reason`;
15
+ * · `field`:体内坐标 `a2aPeers[i].<键路径>`(条目不是对象 ⇒ `a2aPeers[i]`;撞帽 ⇒ `a2aPeers`),与受理口拒体 `field` 同一个坐标系。
16
+ * 与 MCP 面的差只有两处,都是各自协议的事实:MCP 的 `reason` 是引擎词表的闭集词(`malformed_entry` …),A2A 引擎没有这张词表,`reason` 恒是句子(判官的答案 / 撞帽句);MCP 的 `field` 是条目内键路径,
17
+ * A2A 的是体内绝对坐标(它的拒体 `field` 本就如此)。
18
+ */
19
+ export interface A2aInjectionDrop {
20
+ readonly peer: string;
21
+ readonly reason: string;
22
+ readonly field: string;
55
23
  }
24
+ /** The deployment-facing shape this module reads: the principal axis only (the single-user gate). */
25
+ export type A2aGateConfig = PrincipalAxisInput;
56
26
  /**
57
- * Does THIS deployment honor caller-supplied per-request A2A peers? Three vetoes, ALL of them here (this
58
- * predicate is the single owner — the capability bit and the request leg both read it, so there is exactly
59
- * one place where the answer can be wrong):
60
- * ① the deployment locks the `a2a` key — a locked deployment that advertised `a2aInjection:true` would
61
- * hand every consumer an affordance whose every use 400s (`config.locked_key`); "says yes ⟺ route
62
- * works" breaks on the spot. Same conjunction, same reason, as `mcpInjectionHonored`'s lock veto;
63
- * ② the compliance posture denies `a2a_peers` — advertised-but-refused again;
64
- * ③ multi-tenant (deployment-shape row `multiTenant`) — a tenant cannot point the shared worker at an agent.
65
- *
66
- * ⚠️ **`resolveComplianceDenies` is deliberately NOT wrapped in try/catch** (same ruling as the MCP twin):
67
- * it is fail-loud on a posture outside the closed set, and the posture has already been validated twice
68
- * before reaching here (config parse + boot assembly). If it ever throws here, someone bypassed both doors
69
- * and the LOUD direction (route 500) is the correct one — swallowing it into a boolean would let a
70
- * deployment with a broken posture keep advertising the capability (仓规:安全轴禁静默兜底).
27
+ * Does THIS deployment honor caller-supplied per-request A2A peers? One veto — multi-tenant (deployment-shape row `multiTenant`): a tenant cannot
28
+ * point the shared worker at an agent. The capability bit and the request leg both read this predicate, so there is exactly one place where the answer can be wrong.
71
29
  */
72
30
  export declare function a2aInjectionHonored(config: A2aGateConfig): boolean;
31
+ /**
32
+ * 请求体 `a2aPeers[i]` 的**键闭集**(判序 = 本数组序:名 → 对端地址 → 它的 agent card 地址 → 凭据头 → 身份头 → 技能收窄 → 轴覆写)。两向钉在 core 的 `A2aServerSpec` 上:
33
+ * 型上多一个键而表没跟 ⇒ 编译红(新键会被词表外的键门 400 掉 —— wire 上的静默回归);表里写一个型上没有的词 ⇒ 也红。于是这张表是从 core 的型**派生**出来的数据,而不是谁记得去同步的清单。
34
+ */
35
+ export declare const A2A_PEER_ENTRY_KEYS: readonly ["name", "url", "cardUrl", "headers", "principalHeader", "allowSkills", "toolAxes"];
36
+ /**
37
+ * **请求入口的一次读**(`boot/resolve-spec.ts` 在腿首铸一份,受理门与读器——续跑重放腿只有读器——都吃这一份):`body.a2aPeers` 若是数组,返回同形数组
38
+ * (每个对象条目换成它的 {@link PeerSnapshot};非对象条目原样;长度保真 —— 界外的条数仍是受理门撞帽拒与读器丢弃留声的依据,只快照界内的条目);不是数组(缺席 / `null` / 别的形)原样返回。
39
+ * 为什么在入口铸而不是各调用点各铸:快照按调用点铸,受理口一份、读器一份,嵌入方递来的 getter 对象在两次读之间变形(第一次给干净 URL、第二次给带凭据 URL;`headers` 成员第一次
40
+ * `Bearer accepted`、第二次 `Bearer changed`)就让门判第一份、读器挂第二份 —— 门独有的凭据一问被绕过、鉴权头被换掉。入口铸一次、往下传,「门判的就是读器挂的」才字面成立;
41
+ * 快照是纯数据到叶子(见 {@link PeerSnapshot}),后续任何读者都不再触碰源对象。
42
+ */
43
+ export declare function snapshotRequestA2a(raw: unknown, options: {
44
+ readonly reread: boolean;
45
+ }): unknown;
73
46
  /** 读这条对端的是哪条腿:`fresh` = 新提交;`resume` = 续跑重放升级前存下的请求体(`boot/resolve-spec.ts` 的 `opts.leg` 分流,同一个读法)。 */
74
47
  type A2aRequestLeg = "fresh" | "resume";
75
48
  /**
76
49
  * Shape-check the raw `body.a2aPeers`. `null` → a non-array (caller error; the route may 400). Otherwise the
77
- * valid subset + the names of dropped (malformed/over-cap) entries.
50
+ * valid subset + the dropped (malformed/over-cap) entries as {@link A2aInjectionDrop} facts (peer name · reason · body coordinate).
78
51
  */
79
52
  export declare function validateRequestA2a(raw: unknown): {
80
53
  ok: A2aServerSpec[];
81
- dropped: string[];
54
+ dropped: A2aInjectionDrop[];
82
55
  } | null;
83
- /** fresh 提交腿的 `a2aPeers` URL 门:{@link requestA2aPeerUrlRefusal} 有答案 ⇒ 同步 400(码 / 句 / 机读位原样)。 */
84
- export declare function assertRequestA2aPeerUrls(bodyA2a: unknown): void;
85
56
  /**
86
- * DESIGN-269 §2.4 —— **同步**拒面:部署锁了 `a2a` 而请求仍带 `a2aPeers` ⇒ 400 `config.locked_key`
87
- * (逐字沿用 `assertRequestMcpUnlocked` 的裁定,连错误码都同一个 —— 消费端判的是「某个键被行政锁住了」,
88
- * 不是「哪个键」;码分裂只会让 SDK 多写一条等价分支)。
89
- *
90
- * 判据是「请求**占位**」而不是「请求的值合不合法」:锁是两态的,占了就整拒,不静默丢(静默丢正是把
91
- * 判决变成探针的那一形)。键缺席 / 显式 `null` 都不是占位;**空数组是占位**(调用方确实写了这个键)。
92
- *
93
- * ⚠️ **不看车道**:多租户腿本来就会忽略 body peers(`a2aInjectionHonored` 关),但锁在场时「忽略」是
94
- * 错的失败方向 —— operator 声明了「本部署不收任务自带 A2A peer」,那就该说出来。
57
+ * 请求体 `a2aPeers`「这份声明能不能原样用」的回答者(与 MCP 的 `requestMcpDeclarationRefusal` 同一副骨架、同一个拒形):
58
+ * ① **列表**:在场却不是数组 / 超过 {@link MAX_REQUEST_A2A_PEERS} 条(撞帽机读位一组键)/ 某一条不是对象 ⇒ `400 request.field_invalid`;
59
+ * ② **键名**:条目上词表外的键 ⇒ `400 request.body_shape` + `unknownKeys`;
60
+ * ③ **键值**:逐键问判官表 {@link A2A_PEER_KEY_JUDGES} ⇒ `400 request.field_invalid`,拒句点名 `a2aPeers[i].<键路径>`、peer 名与该键判官的原因句;`url` / `cardUrl` 带凭据时拒体另带机读 `field` / `reason`。
61
+ * 整份拒而不是挂前 32 条 / 丢掉读不出的那条 / 整只忽略:一份被悄悄截短、少了几条或整只没生效的声明,在发送方那里读起来是「生效了」(修前:这些形全部 200 + 运维 warn;其中 `allowSkills`
62
+ * 写成 `allowSkill` 的对端照样挂上远端卡声明的全部技能 —— 收窄意图坏形时退化成放行)。同时这份体整份落盘,界内每一条的 `url` / `cardUrl` 都被判官看过 —— 超过界的列表整份拒,所以扫描有界。
95
63
  *
96
- * ⚠️ 参数类型是 `ReadonlySet<string>`:core 的 `LockedKey` 尚无 `a2a` 成员(见模块头),而
97
- * `ReadonlySet<LockedKey>` 结构上可当 `ReadonlySet<string>` 传入,所以调用点无需任何转换。
64
+ * 纯值域、与租户车道无关(多租户腿整只 `a2aPeers` 本会被忽略,告诉调用方「这样写不行」只多不少)。
65
+ * 只在 fresh 门上判(`boot/resolve-spec.ts` 在 MCP 条目门旁):重放存量 body 的腿不问它 —— 拒会砖死升级前停驻的任务(本仓 `opts.leg` 先例),走宽容读器({@link resolveRequestA2a}:坏条目不挂 +
66
+ * `a2a_injection_dropped` 留声)。A2A serve 腿(`routes/a2a-serve.ts`)铸的体只有 `objective` / `taskId` / `sessionId` / `scenario` 四键,到不了这一座。
98
67
  */
99
- export declare function assertRequestA2aUnlocked(bodyA2a: unknown, lockedKeys: ReadonlySet<string>): void;
68
+ export declare function requestA2aDeclarationRefusal(list: unknown): DeclarationRefusal | undefined;
69
+ /** fresh 提交腿的 `a2aPeers` 门:{@link requestA2aDeclarationRefusal} 有答案 ⇒ 同步 400(码 / 句 / `unknownKeys` / 机读位原样)。 */
70
+ export declare function assertRequestA2aEntries(bodyA2a: unknown): void;
100
71
  /**
101
72
  * Merge GATED per-request peers OVER the deployment baseline (center/config). The baseline WINS on a name
102
73
  * clash — a caller can ADD a peer but can never SHADOW a configured one. This is load-bearing on THIS lane