@sema-agent/server 7.137.0 → 7.138.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 (209) hide show
  1. package/MIGRATION.md +328 -139
  2. package/README.md +1 -0
  3. package/README.zh-CN.md +1 -0
  4. package/USAGE.md +67 -20
  5. package/dist/approval-card.d.ts +21 -14
  6. package/dist/approval-card.js +1 -0
  7. package/dist/audit.js +1 -0
  8. package/dist/background-agent-name.d.ts +5 -0
  9. package/dist/background-agent-name.js +5 -0
  10. package/dist/boot/budget-tracing.d.ts +1 -12
  11. package/dist/boot/deployment-mods.d.ts +46 -16
  12. package/dist/boot/deployment-mods.js +82 -6
  13. package/dist/boot/governance-seams.d.ts +10 -17
  14. package/dist/boot/governance-seams.js +5 -17
  15. package/dist/boot/leader.d.ts +3 -0
  16. package/dist/boot/leader.js +1 -0
  17. package/dist/boot/org-delivery.js +15 -2
  18. package/dist/boot/resolve-spec.d.ts +3 -0
  19. package/dist/boot/resolve-spec.js +23 -12
  20. package/dist/boot/retention-lane.d.ts +11 -5
  21. package/dist/boot/retention-lane.js +2 -2
  22. package/dist/boot/runner-deps.d.ts +7 -3
  23. package/dist/boot/runner-deps.js +1 -0
  24. package/dist/boot/stage-04-stores.d.ts +1 -12
  25. package/dist/boot/stage-05-execution-env.d.ts +3 -12
  26. package/dist/boot/stage-05-execution-env.js +7 -4
  27. package/dist/boot/stage-06-runners.d.ts +3 -12
  28. package/dist/boot/stage-06-runners.js +5 -2
  29. package/dist/boot/stage-07-capability-layer.d.ts +3 -12
  30. package/dist/boot/stage-07-capability-layer.js +2 -2
  31. package/dist/boot/stage-08-reapers.d.ts +3 -12
  32. package/dist/boot/stage-09-leader.d.ts +3 -12
  33. package/dist/boot/stage-09-leader.js +2 -2
  34. package/dist/boot/stage-10-http-server.d.ts +3 -12
  35. package/dist/boot/stage-10-http-server.js +4 -2
  36. package/dist/boot/web-admin.d.ts +5 -2
  37. package/dist/boot/web-admin.js +3 -3
  38. package/dist/brain.js +5 -5
  39. package/dist/budget.d.ts +17 -15
  40. package/dist/budget.js +2 -2
  41. package/dist/capabilities/mcp-probe.d.ts +36 -11
  42. package/dist/capabilities/mcp-probe.js +44 -5
  43. package/dist/capability-facts.d.ts +6 -0
  44. package/dist/capability-facts.js +5 -0
  45. package/dist/checkpoint-identity.d.ts +13 -6
  46. package/dist/config-catalog.js +7 -1
  47. package/dist/config-types.d.ts +15 -7
  48. package/dist/config.js +8 -5
  49. package/dist/deployment-shape.d.ts +6 -6
  50. package/dist/gate-settlement.d.ts +71 -0
  51. package/dist/gate-settlement.js +40 -0
  52. package/dist/hooks/classic-stations.d.ts +1 -0
  53. package/dist/hooks/classic-stations.js +1 -0
  54. package/dist/hooks/command-spawn.d.ts +2 -2
  55. package/dist/hooks/hook-runner.js +14 -14
  56. package/dist/http/admission.js +4 -2
  57. package/dist/http/dispatch.d.ts +4 -0
  58. package/dist/http/dispatch.js +3 -0
  59. package/dist/http/durable-run-leg.js +4 -2
  60. package/dist/http/memory-compliance-failure-reply.js +3 -0
  61. package/dist/http/resume-legs.d.ts +5 -11
  62. package/dist/http/resume-legs.js +136 -91
  63. package/dist/http/route-ctx.d.ts +30 -51
  64. package/dist/http/route-ctx.js +2 -5
  65. package/dist/http/routes/a2a-serve.js +10 -4
  66. package/dist/http/routes/agents-roster.d.ts +1 -1
  67. package/dist/http/routes/agents-roster.js +5 -1
  68. package/dist/http/routes/approvals-assistant.js +6 -6
  69. package/dist/http/routes/capabilities.js +12 -10
  70. package/dist/http/routes/mcp-reconnect.d.ts +3 -2
  71. package/dist/http/routes/mcp-reconnect.js +7 -5
  72. package/dist/http/routes/memory-consolidation.d.ts +1 -1
  73. package/dist/http/routes/memory-origin.d.ts +1 -1
  74. package/dist/http/routes/rules.js +3 -3
  75. package/dist/http/routes/runs.js +11 -7
  76. package/dist/http/routes/sessions.js +6 -4
  77. package/dist/http/routes/side-query.js +2 -3
  78. package/dist/http/routes/tasks.js +792 -780
  79. package/dist/http/server.d.ts +31 -25
  80. package/dist/http/wire-types.d.ts +14 -6
  81. package/dist/identity-source.d.ts +1 -1
  82. package/dist/index.d.ts +2 -0
  83. package/dist/index.js +1 -0
  84. package/dist/leader/wire.d.ts +7 -0
  85. package/dist/leader/wire.js +5 -4
  86. package/dist/mcp-assertion-header.d.ts +51 -40
  87. package/dist/mcp-assertion-header.js +10 -11
  88. package/dist/mcp-authorization-face.d.ts +27 -0
  89. package/dist/mcp-authorization-face.js +15 -0
  90. package/dist/memory-operator-faces.d.ts +13 -4
  91. package/dist/memory-operator-faces.js +1 -17
  92. package/dist/memory-sync-client.js +72 -57
  93. package/dist/memory-sync.d.ts +1 -1
  94. package/dist/model-route-gap.d.ts +6 -22
  95. package/dist/model-route-gap.js +7 -5
  96. package/dist/mods-declaration.d.ts +110 -0
  97. package/dist/mods-declaration.js +109 -0
  98. package/dist/observability/err-text.d.ts +5 -0
  99. package/dist/observability/err-text.js +9 -0
  100. package/dist/observability/fail-open.d.ts +5 -1
  101. package/dist/observability/fail-open.js +5 -1
  102. package/dist/observability/secret-env-scrub.d.ts +1 -1
  103. package/dist/observability/secret-env-scrub.js +1 -1
  104. package/dist/parked-decide.d.ts +15 -2
  105. package/dist/parked-decide.js +14 -8
  106. package/dist/peer-seat.d.ts +150 -0
  107. package/dist/peer-seat.js +239 -0
  108. package/dist/permission-rule-vocab.d.ts +7 -7
  109. package/dist/permission-rule-vocab.js +3 -3
  110. package/dist/plain-string-record.d.ts +38 -0
  111. package/dist/plain-string-record.js +38 -0
  112. package/dist/plugins/background-agent-store-sql.js +6 -3
  113. package/dist/plugins/caching-session-store.d.ts +5 -0
  114. package/dist/plugins/caching-session-store.js +6 -0
  115. package/dist/plugins/checkpoint-store-sql.d.ts +15 -20
  116. package/dist/plugins/checkpoint-store-sql.js +8 -8
  117. package/dist/plugins/file-history-store-sql.js +3 -2
  118. package/dist/plugins/k8s-pod-reaper.d.ts +2 -8
  119. package/dist/plugins/k8s-pod-reaper.js +3 -3
  120. package/dist/plugins/local-checkpoint-store.d.ts +1 -11
  121. package/dist/plugins/local-checkpoint-store.js +0 -1
  122. package/dist/plugins/local-session-store.d.ts +66 -27
  123. package/dist/plugins/local-session-store.js +60 -28
  124. package/dist/plugins/mailbox-store-sql.d.ts +11 -3
  125. package/dist/plugins/mailbox-store-sql.js +103 -15
  126. package/dist/plugins/memory-sync-store-pg.d.ts +16 -5
  127. package/dist/plugins/memory-sync-store-tidb.d.ts +3 -3
  128. package/dist/plugins/one-time-migrations.d.ts +6 -3
  129. package/dist/plugins/one-time-migrations.js +3 -4
  130. package/dist/plugins/permission-rule-store-sql.js +21 -13
  131. package/dist/plugins/pg-session-storage.d.ts +10 -2
  132. package/dist/plugins/pg-session-storage.js +18 -5
  133. package/dist/plugins/remote-env-adb.d.ts +3 -1
  134. package/dist/plugins/remote-env-adb.js +1 -1
  135. package/dist/plugins/remote-env-device.d.ts +3 -1
  136. package/dist/plugins/remote-env-device.js +1 -1
  137. package/dist/plugins/remote-env-e2b.d.ts +2 -0
  138. package/dist/plugins/remote-env-e2b.js +1 -1
  139. package/dist/plugins/remote-env-host.d.ts +3 -1
  140. package/dist/plugins/remote-env-host.js +1 -1
  141. package/dist/plugins/remote-env-k8s.d.ts +3 -1
  142. package/dist/plugins/remote-env-k8s.js +1 -1
  143. package/dist/plugins/remote-env-local-docker.d.ts +2 -0
  144. package/dist/plugins/remote-env-local-docker.js +1 -1
  145. package/dist/plugins/remote-env-ssh.d.ts +3 -1
  146. package/dist/plugins/remote-env-ssh.js +1 -1
  147. package/dist/plugins/retention-store-sql.d.ts +14 -2
  148. package/dist/plugins/retention-store-sql.js +6 -0
  149. package/dist/plugins/session-fork.d.ts +10 -13
  150. package/dist/plugins/session-fork.js +5 -25
  151. package/dist/plugins/session-ownership.d.ts +7 -0
  152. package/dist/plugins/session-ownership.js +5 -0
  153. package/dist/plugins/session-workspace-cell.d.ts +17 -0
  154. package/dist/plugins/session-workspace-cell.js +38 -0
  155. package/dist/plugins/store-backend.js +7 -7
  156. package/dist/plugins/tidb-pool.js +6 -0
  157. package/dist/plugins/tidb-session-store.d.ts +17 -2
  158. package/dist/plugins/tidb-session-store.js +16 -5
  159. package/dist/plugins/tool-result-store-sql.d.ts +13 -6
  160. package/dist/plugins/tool-result-store-sql.js +8 -7
  161. package/dist/plugins/worktree-isolation.d.ts +3 -2
  162. package/dist/plugins/worktree-isolation.js +1 -7
  163. package/dist/project-identity.d.ts +1 -14
  164. package/dist/project-identity.js +0 -4
  165. package/dist/rules-consent.js +6 -4
  166. package/dist/run-local.js +12 -6
  167. package/dist/runs.d.ts +7 -1
  168. package/dist/runs.js +6 -1
  169. package/dist/sandbox-git-plane.d.ts +2 -2
  170. package/dist/server-secret-env.d.ts +6 -3
  171. package/dist/server-secret-env.js +4 -2
  172. package/dist/session-incarnation.d.ts +4 -5
  173. package/dist/session-incarnation.js +2 -2
  174. package/dist/session-workspace.d.ts +130 -0
  175. package/dist/session-workspace.js +83 -0
  176. package/dist/spec-fields.d.ts +1 -1
  177. package/dist/standing-answer-grant.d.ts +2 -13
  178. package/dist/standing-answer-grant.js +0 -7
  179. package/dist/task-a2a.d.ts +44 -14
  180. package/dist/task-a2a.js +46 -20
  181. package/dist/task-mcp.d.ts +33 -20
  182. package/dist/task-mcp.js +46 -22
  183. package/dist/tool-approval.d.ts +32 -8
  184. package/dist/tool-approval.js +49 -12
  185. package/dist/trace/core-keyset-guard.d.ts +33 -18
  186. package/dist/trace/engine-notice-wire.d.ts +1 -1
  187. package/dist/trace/engine-notice-wire.js +2 -0
  188. package/dist/trace/exit-seat.d.ts +20 -1
  189. package/dist/trace/exit-seat.js +3 -0
  190. package/dist/trace/extension-call-face.d.ts +33 -0
  191. package/dist/trace/extension-call-face.js +39 -0
  192. package/dist/trace/project.d.ts +25 -7
  193. package/dist/trace/project.js +29 -3
  194. package/dist/trace/redact.js +42 -13
  195. package/dist/trace/self-minted-wire.d.ts +129 -10
  196. package/dist/trace/self-minted-wire.js +62 -3
  197. package/dist/trace/wire-projection-faces.d.ts +6 -6
  198. package/dist/trace/wire-projection-faces.js +6 -5
  199. package/dist/user-authored-text.d.ts +2 -2
  200. package/dist/user-authored-text.js +16 -1
  201. package/dist/web-admin-client/center-request.d.ts +3 -2
  202. package/dist/web-admin-client/center-request.js +2 -3
  203. package/dist/web-admin-client/skills-mcp.d.ts +8 -4
  204. package/dist/web-admin-client/skills-mcp.js +31 -18
  205. package/dist/web-admin-client/types.d.ts +5 -0
  206. package/package.json +3 -3
  207. package/wire-changes/server@7.138.0.json +446 -0
  208. package/dist/http/mcp-remote-text.d.ts +0 -5
  209. package/dist/http/mcp-remote-text.js +0 -7
@@ -14,7 +14,7 @@ export const SECRET_ENV_SCRUB_SITES = {
14
14
  remedy: null,
15
15
  },
16
16
  "server.hooks.spawn-env": {
17
- note: "settings hooks 的命令腿(请求带来的命令跑在宿主上)的 env —— 严格名单(`PATH` / `HOME` / 终端 / locale 等十二名)经引擎组合,名单外一律不交;请求显式给的 `settings.env` 另叠在上面",
17
+ note: "settings hooks 的命令腿(请求带来的命令跑在宿主上)的 env —— 严格名单(`PATH` / `HOME` / 终端 / locale 等十二名)经引擎的 `hostChildEnv` 组合:名单外一律不交,本服务自有凭据任何情况都不交;请求的有效环境层(`settings.env`)作授予叠在上面;请求给工具 shell 的继承词对它不作用",
18
18
  remedy: null,
19
19
  },
20
20
  "server.project-memory.git-subprocess": {
@@ -16,6 +16,7 @@ import type { RebuiltInheritedGate } from "./boot/parked-revive-gate.js";
16
16
  import { type WorkflowRunParkKey, type WorkflowAgentSessionIndex } from "./orchestration/workflow-agent-session-index.js";
17
17
  import type { WorkflowParkDecisionStore, WorkflowParkOutcome } from "./orchestration/workflow-park-decision.js";
18
18
  import { type HeadRead } from "./session-incarnation.js";
19
+ import type { GateIdentity } from "./checkpoint-identity.js";
19
20
  export interface ParkedAgentMatch {
20
21
  handle: string;
21
22
  row: BackgroundAgentRecord;
@@ -224,6 +225,18 @@ export interface ParkedDecideRequest {
224
225
  * 的挂点(调用方自吞错,绝不抛落赎回)。claim 败/判别 miss 恒不触发。 */
225
226
  onDecideCommitted?: () => Promise<void>;
226
227
  }
228
+ /**
229
+ * 后台子代赎回腿({@link decideParkedAgent})收的请求 = 两条车道共用的那一份 + 选中这张卡的**门身份片段**。workflow 车道是投递、不收它
230
+ * (那条车道的回执不带结清证据,身份在那里不适用 —— 不捕获,也就不为它记「缺身份」)。
231
+ */
232
+ export interface ParkedAgentDecideRequest extends ParkedDecideRequest {
233
+ /**
234
+ * 调用方在执行之前从选中的那一行捕获、已过信任边界(`checkpoint-identity.ts` 的 `durableRowIdentity`,face `decision-receipt`;读不出 ⇒ `{}`)。
235
+ * 本腿的两个结清边界(复活成功 / 不可复活 deny 作废)把它随结清证据上体(`gate-settlement.ts`)。必填:身份只有这一个来源 ——
236
+ * agent 行上没有这一格,本腿也不另读「当前待决门」(受理之后它可能已是下一张卡);认领到的 token 不是选中的那一张 ⇒ 拒,不替它作证。
237
+ */
238
+ readonly selectedGate: GateIdentity;
239
+ }
227
240
  export interface ParkedDecideResult {
228
241
  status: number;
229
242
  body: Record<string, unknown>;
@@ -341,8 +354,8 @@ export declare function planWorkflowParkedDecide(origin: Extract<ParkOrigin, {
341
354
  * ([ref] Q2:isError 入参级早退 + throw 两形,已核幂等/双滚=无害 lost)。
342
355
  *
343
356
  * 200 是**受理**语义(drive 是 core 内 fire-and-forget):失败经行/notification 面 + REOPENED
344
- * re-park 诚实可见,操作员可再决。
357
+ * re-park 诚实可见,操作员可再决。200 体 `{ taskId, status: "resuming", decision }` + 结清证据(`gate-settlement.ts` 的边界 ④)。
345
358
  */
346
- export declare function decideParkedAgent(deps: ParkedDecideDeps, req: ParkedDecideRequest, match: ParkedAgentMatch): Promise<ParkedDecideResult>;
359
+ export declare function decideParkedAgent(deps: ParkedDecideDeps, req: ParkedAgentDecideRequest, match: ParkedAgentMatch): Promise<ParkedDecideResult>;
347
360
  export {};
348
361
  //# sourceMappingURL=parked-decide.d.ts.map
@@ -7,6 +7,8 @@ import { backgroundScopesForCheckpointScope, decodeCheckpointScope, ownerOfCheck
7
7
  import { ASK_USER_QUESTION_TOOL_NAME, isEngineQuestionRow } from "./approval-content-kind.js";
8
8
  import { sessionFaceOfHead } from "./session-incarnation.js";
9
9
  import { refusalBody, resumeRefusalFacts } from "./http/refusal-body.js";
10
+ import { backgroundAgentNameOf } from "./background-agent-name.js";
11
+ import { buildSettlementEvidence } from "./gate-settlement.js";
10
12
  export async function findParkOrigin(sources, cp) {
11
13
  if (sources.agentStore !== undefined) {
12
14
  let match;
@@ -111,7 +113,7 @@ function mintReviveArgs(row, knownAgentTypes) {
111
113
  return {
112
114
  prompt: row.description ?? "resume parked agent",
113
115
  run_in_background: true,
114
- name: row.name,
116
+ name: backgroundAgentNameOf(row),
115
117
  description: row.description ?? "revived agent",
116
118
  ...(row.agentType !== undefined && knownAgentTypes.has(row.agentType) ? { subagent_type: row.agentType } : {}),
117
119
  };
@@ -232,10 +234,10 @@ function sessionGone(taskId) {
232
234
  }),
233
235
  };
234
236
  }
237
+ function gateMoved(taskId) {
238
+ return { status: 409, body: refusalBody({ errorCode: "decide.gate_moved", message: "the approval gate moved under this decide (re-parked or already settled) — re-check the pending list", facts: { taskId } }) };
239
+ }
235
240
  function revivabilityOf(row, taskId) {
236
- if (row.name === undefined) {
237
- return { kind: "unrevivable", approveRefusal: { status: 409, body: refusalBody({ errorCode: "decide.row_unrevivable", message: "parked row carries no agent name — the revive path requires a named agent, so this approval cannot be approved; deny it to close the card (the agent then ends as failed)", facts: { taskId } }) } };
238
- }
239
241
  if (row.rootSessionId === undefined) {
240
242
  return { kind: "unrevivable", approveRefusal: { status: 409, body: refusalBody({ errorCode: "decide.row_unrevivable", message: "this agent's row carries no root session anchor — the revive path cannot tell which session this approval belongs to, so it cannot be approved; deny it to close the card (the agent then ends as failed)", facts: { taskId } }) } };
241
243
  }
@@ -244,7 +246,7 @@ function revivabilityOf(row, taskId) {
244
246
  async function voidUnrevivableCard(deps, req, match) {
245
247
  const voided = await deps.checkpointStore.expire(req.token, req.scope);
246
248
  if (!voided) {
247
- return { status: 409, body: refusalBody({ errorCode: "decide.gate_moved", message: "the approval gate moved under this decide (re-parked or already settled) — re-check the pending list", facts: { taskId: match.handle } }) };
249
+ return gateMoved(match.handle);
248
250
  }
249
251
  try {
250
252
  await deps.registry.reconcileParkedAgents({ agentStore: deps.agentStore, checkpointStore: deps.checkpointStore }, match.row.scope, Date.now());
@@ -252,7 +254,7 @@ async function voidUnrevivableCard(deps, req, match) {
252
254
  catch (e) {
253
255
  quiet(deps.warn, "parked_unrevivable_reconcile_failed", { taskId: match.handle, error: e instanceof Error ? e.message : String(e) });
254
256
  }
255
- return { status: 200, body: { taskId: match.handle, status: "failed", decision: "deny" } };
257
+ return { status: 200, body: { taskId: match.handle, status: "failed", decision: "deny", ...buildSettlementEvidence({ at: "card_voided", gate: req.selectedGate }) } };
256
258
  }
257
259
  export async function decideParkedAgent(deps, req, match) {
258
260
  const row = match.row;
@@ -282,7 +284,7 @@ export async function decideParkedAgent(deps, req, match) {
282
284
  return { status: 409, body: refusalBody({ errorCode: "decide.claim_lost", message: "the parked agent is already claimed — either a concurrent decide won (re-check the pending list), or a stale claim is pending automatic cleanup (retry after the stale-claim window)", facts: { taskId: match.handle } }) };
283
285
  case "binding_broken":
284
286
  case "checkpoint_not_pending":
285
- return { status: 409, body: refusalBody({ errorCode: "decide.gate_moved", message: "the approval gate moved under this decide (re-parked or already settled) — re-check the pending list", facts: { taskId: match.handle } }) };
287
+ return gateMoved(match.handle);
286
288
  default:
287
289
  return { status: 409, body: refusalBody({ errorCode: "decide.not_parked", message: "the parked agent moved under this decide (stopped or already decided)", facts: { taskId: match.handle } }) };
288
290
  }
@@ -306,6 +308,10 @@ export async function decideParkedAgent(deps, req, match) {
306
308
  return "retry";
307
309
  }
308
310
  };
311
+ if (ticket.token !== req.token) {
312
+ await rollback("claimed_token_moved");
313
+ return gateMoved(match.handle);
314
+ }
309
315
  let faceAfter;
310
316
  try {
311
317
  faceAfter = sessionFaceOfHead(root, await deps.headOf(root));
@@ -408,6 +414,6 @@ export async function decideParkedAgent(deps, req, match) {
408
414
  }),
409
415
  };
410
416
  }
411
- return { status: 200, body: { taskId: match.handle, status: "resuming", decision: req.decision } };
417
+ return { status: 200, body: { taskId: match.handle, status: "resuming", decision: req.decision, ...buildSettlementEvidence({ at: "agent_revived", gate: req.selectedGate }) } };
412
418
  }
413
419
  //# sourceMappingURL=parked-decide.js.map
@@ -0,0 +1,150 @@
1
+ import type { A2aServerSpec, McpServerSpec, PeerAnswer, PeerQuestion, PeerSeatTarget } from "@sema-agent/core";
2
+ import { type Logger } from "./observability/logger.js";
3
+ import { type SafeText } from "./trace/exit-seat.js";
4
+ type StdioArm = Extract<McpServerSpec["transport"], {
5
+ kind: "stdio";
6
+ }>;
7
+ type HttpArm = Extract<McpServerSpec["transport"], {
8
+ kind: "http";
9
+ }>;
10
+ /** The pieces a declaration's sends carry, as written: header name → value (http MCP, A2A) or variable → value (stdio MCP). */
11
+ export type PeerPieces = Readonly<Record<string, string>>;
12
+ /**
13
+ * An MCP server declaration before the desk has seen it: the engine's declaration without its credential reference, beside the
14
+ * pieces. `credentials?: never` sits on each transport arm (where the engine keeps the reference) so that a declaration that already
15
+ * carries one cannot be passed back in as a draft: `Omit` alone would not refuse it, because excess members are judged only on fresh
16
+ * object literals. Without `exactOptionalPropertyTypes` an explicit `credentials: undefined` still passes; it carries no reference.
17
+ */
18
+ export type McpDraftSpec = Omit<McpServerSpec, "transport"> & {
19
+ transport: (Omit<StdioArm, "credentials"> & {
20
+ credentials?: never;
21
+ }) | (Omit<HttpArm, "credentials"> & {
22
+ credentials?: never;
23
+ });
24
+ };
25
+ /** An MCP draft: the declaration and the pieces its sends carry (absent or empty ⇒ nothing to register). */
26
+ export interface McpDraft {
27
+ readonly spec: McpDraftSpec;
28
+ readonly pieces?: PeerPieces;
29
+ }
30
+ /** An A2A peer declaration before the desk has seen it; the engine keeps an A2A peer's reference at the top level. */
31
+ export type A2aDraftSpec = Omit<A2aServerSpec, "credentials"> & {
32
+ credentials?: never;
33
+ };
34
+ /** An A2A draft: the declaration and the headers its sends carry (absent or empty ⇒ nothing to register). */
35
+ export interface A2aDraft {
36
+ readonly spec: A2aDraftSpec;
37
+ readonly pieces?: PeerPieces;
38
+ }
39
+ export type PeerDraft = McpDraft | A2aDraft;
40
+ /** The reference {@link PeerDesk.declare} mints (the `handle` kind of the reference the engine asks with): opaque, the same for the same
41
+ * content within one process. */
42
+ export type PeerHandle = Extract<NonNullable<PeerSeatTarget["credentials"]>, {
43
+ kind: "handle";
44
+ }>;
45
+ /**
46
+ * Who a send goes to, as the seat binds an answer to it: the engine's question target without the reference itself and without an
47
+ * A2A send's `destination` (the engine owns which declared origin a send may reach). An http MCP server = name, url, mount
48
+ * fingerprint; a stdio one = name, the whole start command (absent `args` ≡ `[]`), fingerprint; an A2A peer = name, url, `cardUrl`.
49
+ */
50
+ export type PeerBinding = {
51
+ readonly protocol: "mcp";
52
+ readonly transport: "http";
53
+ readonly server: string;
54
+ readonly url: string;
55
+ readonly fingerprint?: string;
56
+ } | {
57
+ readonly protocol: "mcp";
58
+ readonly transport: "stdio";
59
+ readonly server: string;
60
+ readonly command: string;
61
+ readonly args: readonly string[];
62
+ readonly fingerprint?: string;
63
+ } | {
64
+ readonly protocol: "a2a";
65
+ readonly peer: string;
66
+ readonly url: string;
67
+ readonly cardUrl?: string;
68
+ };
69
+ /** Why the seat refused a question. Closed: a new word is a new arm of `answer`'s switch. */
70
+ export type PeerSeatRefusalReason = "handle_not_held" | "binding_mismatch" | "kind_env" | "kind_vault" | "kind_assertion" | "kind_unknown";
71
+ /**
72
+ * The desk's own failures, each loud:
73
+ * · `peer_lease_released` — a released lease was used to declare or to extend (a scope released before its last use);
74
+ * · `peer_lease_foreign` — a lease another desk opened was used here (its release would never reach this desk's entries);
75
+ * · `peer_lease_scope_unknown` — a lease was asked for with a scope word outside {@link PEER_LEASE_SCOPES};
76
+ * · `peer_binding_conflict` — two different contents were minted one id (only a digest collision does that); the entry stands;
77
+ * · `peer_seat_refused` — the seat refused a question (the engine reads any throw as the seat not furnishing that send).
78
+ * No message names a value or an id.
79
+ */
80
+ export type PeerDeskErrorCode = "peer_lease_released" | "peer_lease_foreign" | "peer_lease_scope_unknown" | "peer_binding_conflict" | "peer_seat_refused";
81
+ export declare class PeerDeskError extends Error {
82
+ readonly code: PeerDeskErrorCode;
83
+ constructor(code: PeerDeskErrorCode, message: string);
84
+ }
85
+ /**
86
+ * Which host scope a lease is opened for: the request funnel of a fresh submission, the funnel of a resumed leg, a probe walk, the
87
+ * roster (held for the process's life). A closed set, so the desk's own error text can name a scope without echoing caller text.
88
+ */
89
+ export declare const PEER_LEASE_SCOPES: readonly ["fresh", "resume", "probe", "roster"];
90
+ export type PeerLeaseScope = (typeof PEER_LEASE_SCOPES)[number];
91
+ /** A host holder. `release` is idempotent; `extend` opens a further lease that holds every entry this one holds now. */
92
+ export interface PeerLease {
93
+ /** A further lease holding what this one holds at this moment; this one released ⇒ throws `peer_lease_released`. */
94
+ extend(): PeerLease;
95
+ /** Stop holding. Entries no other holder holds are dropped now. A second call does nothing. */
96
+ release(): void;
97
+ }
98
+ /** What the engine asks: the pieces for one send. The seat holds nothing itself — an entry's holders are host leases only. */
99
+ export interface PeerDeskSeat {
100
+ /** The pieces for one send, or a throw (refused: logged as one `peer_seat_refused` row naming protocol, peer and reason only). */
101
+ answer(question: PeerQuestion): Promise<PeerAnswer>;
102
+ }
103
+ /** Live entries and the host leases holding them, summed across entries — the registry's whole observable state. */
104
+ export interface PeerDeskSize {
105
+ readonly entries: number;
106
+ readonly holders: number;
107
+ }
108
+ export interface PeerDesk {
109
+ readonly seat: PeerDeskSeat;
110
+ /**
111
+ * Register a draft's pieces under `lease` and mint the reference the declaration carries to the engine. No pieces ⇒ nothing is
112
+ * registered and nothing is minted (`undefined`: the declaration goes without a reference). The same content (binding, lane,
113
+ * pieces by name) mints the same id in one process, so two legs declaring one body share one entry and each holds it.
114
+ */
115
+ declare(draft: PeerDraft, lease: PeerLease): PeerHandle | undefined;
116
+ /** Open a host lease for `scope` (named in the desk's own error text; a word outside the set ⇒ throws `peer_lease_scope_unknown`). */
117
+ lease(scope: PeerLeaseScope): PeerLease;
118
+ size(): PeerDeskSize;
119
+ }
120
+ export interface PeerDeskOptions {
121
+ /** Where refusal rows go (default: a JSON-line logger). */
122
+ readonly log?: Logger;
123
+ }
124
+ /** The binding of a draft, or of the target the engine asks about; both read the same three rows. */
125
+ export declare function bindingOf(peer: PeerDraft | PeerSeatTarget): PeerBinding;
126
+ export declare function createPeerDesk(opts?: PeerDeskOptions): PeerDesk;
127
+ /**
128
+ * **The one minting point** every entrance calls — the roster (under the roster lease), the request surface (under the leg's host lease) and
129
+ * the probe walk: register `draft`'s pieces on `desk` under `lease` and hand back the declaration the engine gets, carrying the minted
130
+ * reference where the engine keeps it (an MCP server's transport arm, an A2A peer's top level) and no piece value anywhere. A draft with no
131
+ * pieces registers nothing and goes as drafted (no reference member). Where the reference sits is written here and nowhere else.
132
+ */
133
+ export declare function mintDeclaration(desk: Pick<PeerDesk, "declare">, draft: McpDraft, lease: PeerLease): McpServerSpec;
134
+ export declare function mintDeclaration(desk: Pick<PeerDesk, "declare">, draft: A2aDraft, lease: PeerLease): A2aServerSpec;
135
+ /**
136
+ * **The one exit** for an engine judge's sentence about a peer declaration (an MCP server's or an A2A peer's `toolAxes` / `toolFaces` /
137
+ * `sessionMetaKey` / start bound …) on its way into this server's refusal text or an operator line: the engine words what the declaring side
138
+ * wrote (a tool name, a key — which a mistake can make a credential), so the sentence leaves through the exit seat's `peer-declaration-sentence`
139
+ * row (credential shapes become marks, the sentence stays the engine's). The request surface's judges, the replay reread lines and the roster's
140
+ * skip lines all use this one.
141
+ */
142
+ export declare function peerSentence(problem: string): SafeText;
143
+ /**
144
+ * Hold `lease`'s entries for as long as `drive` runs: a further lease is opened before `drive` is called (synchronously, so before
145
+ * the caller's own scope can release) and released when `drive` settles — resolved, rejected or thrown synchronously. A released
146
+ * lease rejects with `peer_lease_released` and `drive` is not called.
147
+ */
148
+ export declare function heldFor<T>(lease: PeerLease, drive: () => Promise<T>): Promise<T>;
149
+ export {};
150
+ //# sourceMappingURL=peer-seat.d.ts.map
@@ -0,0 +1,239 @@
1
+ import { createHmac, randomBytes } from "node:crypto";
2
+ import { createLogger } from "./observability/logger.js";
3
+ import { seatText } from "./trace/exit-seat.js";
4
+ export class PeerDeskError extends Error {
5
+ code;
6
+ constructor(code, message) {
7
+ super(message);
8
+ this.code = code;
9
+ this.name = "PeerDeskError";
10
+ }
11
+ }
12
+ export const PEER_LEASE_SCOPES = ["fresh", "resume", "probe", "roster"];
13
+ export function bindingOf(peer) {
14
+ if ("spec" in peer) {
15
+ const spec = peer.spec;
16
+ if (!("transport" in spec))
17
+ return a2aBinding(spec.name, spec.url, spec.cardUrl);
18
+ const t = spec.transport;
19
+ switch (t.kind) {
20
+ case "http":
21
+ return httpBinding(spec.name, t.url, spec.fingerprint);
22
+ case "stdio":
23
+ return stdioBinding(spec.name, t.command, t.args ?? [], spec.fingerprint);
24
+ default: {
25
+ const unknownArm = t;
26
+ throw new Error(`peer desk: an MCP draft with a transport this desk does not know (${discriminantsOf(unknownArm)})`);
27
+ }
28
+ }
29
+ }
30
+ switch (peer.protocol) {
31
+ case "mcp":
32
+ switch (peer.transport) {
33
+ case "http":
34
+ return httpBinding(peer.server, peer.url, peer.fingerprint);
35
+ case "stdio":
36
+ return stdioBinding(peer.server, peer.command, peer.args, peer.fingerprint);
37
+ default: {
38
+ const unknownArm = peer;
39
+ throw new Error(`peer desk: a question about an MCP transport this desk does not know (${discriminantsOf(unknownArm)})`);
40
+ }
41
+ }
42
+ case "a2a":
43
+ return a2aBinding(peer.peer, peer.url, peer.cardUrl);
44
+ default: {
45
+ const unknownArm = peer;
46
+ throw new Error(`peer desk: a question about a protocol this desk does not know (${discriminantsOf(unknownArm)})`);
47
+ }
48
+ }
49
+ }
50
+ function httpBinding(server, url, fingerprint) {
51
+ return Object.freeze({ protocol: "mcp", transport: "http", server, url, ...(fingerprint !== undefined ? { fingerprint } : {}) });
52
+ }
53
+ function stdioBinding(server, command, args, fingerprint) {
54
+ return Object.freeze({ protocol: "mcp", transport: "stdio", server, command, args: Object.freeze([...args]), ...(fingerprint !== undefined ? { fingerprint } : {}) });
55
+ }
56
+ function a2aBinding(peer, url, cardUrl) {
57
+ return Object.freeze({ protocol: "a2a", peer, url, ...(cardUrl !== undefined ? { cardUrl } : {}) });
58
+ }
59
+ function discriminantsOf(value) {
60
+ if (typeof value !== "object" || value === null)
61
+ return typeof value;
62
+ const at = (key) => {
63
+ const v = Reflect.get(value, key);
64
+ return typeof v === "string" ? JSON.stringify(v.slice(0, 40)) : typeof v;
65
+ };
66
+ return `protocol ${at("protocol")}, transport ${at("transport")}, kind ${at("kind")}`;
67
+ }
68
+ function laneOf(binding) {
69
+ return binding.protocol === "a2a" ? "a2a" : binding.transport;
70
+ }
71
+ function rowsOf(record) {
72
+ return Object.entries(record).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
73
+ }
74
+ function canonicalOf(binding, lane, pieces) {
75
+ return JSON.stringify([rowsOf(binding), lane, rowsOf(pieces)]);
76
+ }
77
+ function sameBinding(a, b) {
78
+ return JSON.stringify(rowsOf(a)) === JSON.stringify(rowsOf(b));
79
+ }
80
+ const ID_KEY = randomBytes(32);
81
+ function mintId(canonical) {
82
+ return `h1.${createHmac("sha256", ID_KEY).update(canonical).digest("base64url")}`;
83
+ }
84
+ function copyPieces(given) {
85
+ if (given === undefined)
86
+ return undefined;
87
+ const names = Object.keys(given);
88
+ if (names.length === 0)
89
+ return undefined;
90
+ const copy = Object.create(null);
91
+ for (const name of names)
92
+ copy[name] = given[name];
93
+ return Object.freeze(copy);
94
+ }
95
+ export function createPeerDesk(opts = {}) {
96
+ const log = opts.log ?? createLogger();
97
+ const table = new Map();
98
+ const leases = new WeakMap();
99
+ let lastLeaseId = 0;
100
+ const entryHeld = (id) => {
101
+ const entry = table.get(id);
102
+ if (entry === undefined)
103
+ throw new Error("peer desk: a live lease holds an entry the table no longer has");
104
+ return entry;
105
+ };
106
+ const dropIfUnheld = (id, entry) => {
107
+ if (entry.hosts.size === 0)
108
+ table.delete(id);
109
+ };
110
+ const liveState = (lease) => {
111
+ const state = leases.get(lease);
112
+ if (state === undefined)
113
+ throw new PeerDeskError("peer_lease_foreign", "peer desk: a lease another desk opened was used here");
114
+ const held = state.held;
115
+ if (held === undefined)
116
+ throw new PeerDeskError("peer_lease_released", `peer desk: a "${state.scope}" lease was used after it was released`);
117
+ return { id: state.id, scope: state.scope, held };
118
+ };
119
+ const openLease = (scope, holds) => {
120
+ const state = { id: ++lastLeaseId, scope, held: new Set(holds) };
121
+ for (const id of holds)
122
+ entryHeld(id).hosts.add(state.id);
123
+ const lease = Object.freeze({
124
+ extend: () => openLease(state.scope, liveState(lease).held),
125
+ release: () => {
126
+ const held = state.held;
127
+ if (held === undefined)
128
+ return;
129
+ state.held = undefined;
130
+ for (const id of held) {
131
+ const entry = entryHeld(id);
132
+ entry.hosts.delete(state.id);
133
+ dropIfUnheld(id, entry);
134
+ }
135
+ },
136
+ });
137
+ leases.set(lease, state);
138
+ return lease;
139
+ };
140
+ const refuse = (target, reason) => {
141
+ const peer = target.protocol === "a2a" ? target.peer : target.server;
142
+ log.warn("peer_seat_refused", { protocol: target.protocol, peer, reason });
143
+ throw new PeerDeskError("peer_seat_refused", `peer desk: the seat refused this send (${reason})`);
144
+ };
145
+ const carry = (entry) => {
146
+ switch (entry.lane) {
147
+ case "http":
148
+ case "a2a":
149
+ return { kind: "carry", headers: entry.pieces };
150
+ case "stdio":
151
+ return { kind: "carry", env: entry.pieces };
152
+ }
153
+ };
154
+ const seat = Object.freeze({
155
+ answer: async (question) => {
156
+ const target = question.peer;
157
+ const ref = target.credentials;
158
+ if (ref === undefined)
159
+ return { kind: "carry" };
160
+ switch (ref.kind) {
161
+ case "handle": {
162
+ const entry = table.get(ref.id);
163
+ if (entry === undefined)
164
+ return refuse(target, "handle_not_held");
165
+ if (!sameBinding(entry.binding, bindingOf(target)))
166
+ return refuse(target, "binding_mismatch");
167
+ return carry(entry);
168
+ }
169
+ case "env":
170
+ return refuse(target, "kind_env");
171
+ case "vault":
172
+ return refuse(target, "kind_vault");
173
+ case "assertion":
174
+ return refuse(target, "kind_assertion");
175
+ default: {
176
+ const unknownKind = ref;
177
+ void unknownKind;
178
+ return refuse(target, "kind_unknown");
179
+ }
180
+ }
181
+ },
182
+ });
183
+ return Object.freeze({
184
+ seat,
185
+ declare: (draft, lease) => {
186
+ const pieces = copyPieces(draft.pieces);
187
+ const binding = pieces === undefined ? undefined : bindingOf(draft);
188
+ const holder = liveState(lease);
189
+ if (pieces === undefined || binding === undefined)
190
+ return undefined;
191
+ const lane = laneOf(binding);
192
+ const canonical = canonicalOf(binding, lane, pieces);
193
+ const id = mintId(canonical);
194
+ const known = table.get(id);
195
+ if (known === undefined) {
196
+ table.set(id, { binding, lane, pieces, hosts: new Set([holder.id]) });
197
+ }
198
+ else if (canonicalOf(known.binding, known.lane, known.pieces) !== canonical) {
199
+ throw new PeerDeskError("peer_binding_conflict", "peer desk: two different declarations were minted one id; the registered one stands");
200
+ }
201
+ else {
202
+ known.hosts.add(holder.id);
203
+ }
204
+ holder.held.add(id);
205
+ return Object.freeze({ kind: "handle", id });
206
+ },
207
+ lease: (scope) => {
208
+ if (!PEER_LEASE_SCOPES.includes(scope))
209
+ throw new PeerDeskError("peer_lease_scope_unknown", "peer desk: a lease was asked for with a scope outside the closed set");
210
+ return openLease(scope, new Set());
211
+ },
212
+ size: () => {
213
+ let holders = 0;
214
+ for (const entry of table.values())
215
+ holders += entry.hosts.size;
216
+ return { entries: table.size, holders };
217
+ },
218
+ });
219
+ }
220
+ export function mintDeclaration(desk, draft, lease) {
221
+ const credentials = desk.declare(draft, lease);
222
+ const spec = draft.spec;
223
+ if ("transport" in spec)
224
+ return credentials === undefined ? spec : { ...spec, transport: { ...spec.transport, credentials } };
225
+ return credentials === undefined ? spec : { ...spec, credentials };
226
+ }
227
+ export function peerSentence(problem) {
228
+ return seatText("peer-declaration-sentence", problem);
229
+ }
230
+ export async function heldFor(lease, drive) {
231
+ const own = lease.extend();
232
+ try {
233
+ return await drive();
234
+ }
235
+ finally {
236
+ own.release();
237
+ }
238
+ }
239
+ //# sourceMappingURL=peer-seat.js.map
@@ -1,14 +1,14 @@
1
1
  import type { RuleBehavior } from "@sema-agent/core";
2
2
  /**
3
- * `PersistedRuleTool` **不再是闭词表**(core 7.9.0 / [ref]):工具名自 core 的**目录**派生
4
- * (`pathTarget` 声明 ⇒ 路径文法;`Bash` ⇒ 命令文法),型是 `string`。于是本文件对它的处置从
5
- * 「转录一份名单」换成「转 core 的**谓词**」——名单在 core 的目录里,抄一份的那天目录加一个带
6
- * `pathTarget` 的工具,本仓会静默拒收一整类合法行(正是本文件顶注说的那个病)。
3
+ * 一张审批卡的**一键整工具同意**能落在这只工具名上吗 —— 仅当没有任何规则词替这个名字说话:它自己没有规则文法(`ruleToolGrammarOf` 答 undefined),
4
+ * 也不在 shell 面上(`COARSE_SHELL_TOOLS`:`Bash` 与 `Monitor`,它们的命令按 `Bash(…)` 规则判)。引擎为卡铸整工具候选恰在这个条件下(没有规则文法、
5
+ * 也不回答任何规则词的调用);一张 shell 卡上的一键整工具同意 = 对每一个程序的常驻许可,路径 / 名字 / 主机工具的卡各有自己的那一形。
7
6
  *
8
- * 谓词方向仍是 fail-CLOSED:`ruleToolGrammarOf` 认不出的名字**不是**一条可持久化规则的工具位
9
- * (既非命令文法也非路径文法 ⇒ 这条行永远匹配不上任何调用),两只店与票口一律拒。
7
+ * 读者:卡素材铸点的派生事实一致性(`tool-approval.ts`:整工具候选的工具过不了 ⇒ 丢整只候选,卡上不渲一格按不动的「不再询问」)。
8
+ * 卡编辑面不读它:引擎的编辑门对任何整工具规则一律拒(编辑只改命令规则),编辑面的形闭集(`rules-consent.ts`)跟着恒拒 —— 那张闭集里「shell 卡上一键整只 shell」
9
+ * 的理由就是本谓词的理由,不另写一份判据。方向 fail-closed:答不出 ⇒ 不是。
10
10
  */
11
- export declare function isPersistedRuleTool(name: string): boolean;
11
+ export declare function cardTakesWholeToolConsent(tool: string): boolean;
12
12
  /**
13
13
  * [ref] —— `RuleBehavior` 每一员在**无同意仪式的直接写口**(`POST /v1/rules`)上的逐员表态。
14
14
  *
@@ -1,6 +1,6 @@
1
- import { ruleToolGrammarOf } from "@sema-agent/core";
2
- export function isPersistedRuleTool(name) {
3
- return ruleToolGrammarOf(name) !== undefined;
1
+ import { COARSE_SHELL_TOOLS, ruleToolGrammarOf } from "@sema-agent/core";
2
+ export function cardTakesWholeToolConsent(tool) {
3
+ return ruleToolGrammarOf(tool) === undefined && !COARSE_SHELL_TOOLS.includes(tool);
4
4
  }
5
5
  export const DIRECT_RULE_WRITE_ADMITS = {
6
6
  allow: false,
@@ -22,4 +22,42 @@ export type PlainStringRecordRead = {
22
22
  * 数据属性取值,访问器调一次。先判每个成员的值(`__proto__` 名的成员也判:它的值不是串 ⇒ 整只不交,与 7.131.x 同),再剔 `__proto__` 名。调用方的对象不被交出去、也不被改。
23
23
  */
24
24
  export declare function readPlainStringRecord(v: unknown): PlainStringRecordRead;
25
+ /**
26
+ * 请求面一份声明里每个键的**去处**(闭集;{@link splitDeclaration} 对它穷尽 `switch`):
27
+ * · `engine` —— 进交给引擎的声明;
28
+ * · `seat` —— 进草稿的件:由部署的对端座在发送时按原字节作答,交给引擎的声明里不带(只有值是「名 → 串」记录的键能标它);
29
+ * · `withheld` —— 承载、不发送:声明与件两边都不进(由声明这一格的属主另说一行)。
30
+ */
31
+ export type DeclarationKeyDisposition = "engine" | "seat" | "withheld";
32
+ /**
33
+ * 请求形 `R` 的**处置表**:`R` 的每个键(可选键也算)恰一格 —— 写成 `as const satisfies DeclarationDispositionTable<R>` 的对象字面量,于是请求形多一个键而表没写 ⇒ 缺属性编译红,
34
+ * 表里写一个请求形上没有的词 ⇒ 多余属性编译红(三道编译格的第一道,两向)。只有值是「名 → 串」记录的键可以标 `seat`(别的键标它 ⇒ 编译红)。
35
+ * 表的书写序就是键闭集的序({@link declarationKeys}):受理门按它判序。
36
+ */
37
+ export type DeclarationDispositionTable<R> = {
38
+ readonly [K in keyof Required<R>]: NonNullable<R[K]> extends Readonly<Record<string, string>> ? DeclarationKeyDisposition : Exclude<DeclarationKeyDisposition, "seat">;
39
+ };
40
+ /** 处置表里标 `engine` 的键。 */
41
+ export type EngineKeysOf<T> = {
42
+ [K in keyof T]: T[K] extends "engine" ? K : never;
43
+ }[keyof T];
44
+ /**
45
+ * 三道编译格的后两道(引擎一侧)的**缺口**,一个型名、两向;用处写成 `AssertAllKeysHandled<EngineSideGap<…>>`(缺口非空 ⇒ 编译红,点名那个键):
46
+ * ② 引擎形上没标 `engine` 的键恰是 `Unbound`(引擎形长出一个键而表没跟 ⇒ 缺口里有它);
47
+ * ③ 标 `engine` 的键与 `Unbound` 都仍在引擎形上(引擎退役一个键、或退役了 `Unbound` 那一位 ⇒ 缺口里有它)。`Unbound` = 引擎形上请求面不写、由宿主铸的位(凭据引用)。
48
+ * 只证键集,不证值型与运行期转换完备(值型由 {@link splitDeclaration} 的返回型交给声明型去判)。
49
+ */
50
+ export type EngineSideGap<T, EngineArm, Unbound extends PropertyKey> = Exclude<keyof EngineArm, EngineKeysOf<T> | Unbound> | Exclude<EngineKeysOf<T> | Unbound, keyof EngineArm>;
51
+ /** 一张处置表的键,按表的书写序(冻结;受理门的键闭集与判序读它,不另写第二份清单)。 */
52
+ export declare function declarationKeys<T extends object>(table: T): ReadonlyArray<keyof T & string>;
53
+ /**
54
+ * 按处置表拆一份请求面声明:标 `engine` 的成员进 `engine`(交给引擎的声明那一部分),标 `seat` 的那一张记录进 `pieces`(空记录 = 没有件,`pieces` 缺席),
55
+ * 标 `withheld` 的成员两边都不进;值为 `undefined` 的成员按缺席读。
56
+ * 请求面声明的唯一拆法 —— MCP 两臂(`toMcpDraft`)与 A2A 对端(`toA2aDraft`)同调这一只,所以「哪个键去哪」只有表一处说,代码里没有第二份键名。
57
+ * 运行期漏臂(响亮):成员不在表里(表的型挡住了写得出的形,只有绕过型的调用方走得到)/ 一份声明有两张标 `seat` 的记录 ⇒ 抛;两句都不带值。
58
+ */
59
+ export declare function splitDeclaration<R extends object, T extends DeclarationDispositionTable<R>>(declaration: R, table: T): {
60
+ readonly engine: Pick<R, EngineKeysOf<T> & keyof R>;
61
+ readonly pieces?: Readonly<Record<string, string>>;
62
+ };
25
63
  //# sourceMappingURL=plain-string-record.d.ts.map
@@ -29,4 +29,42 @@ export function readPlainStringRecord(v) {
29
29
  }
30
30
  return { ok: true, record, stripped };
31
31
  }
32
+ export function declarationKeys(table) {
33
+ return Object.freeze(Object.keys(table));
34
+ }
35
+ export function splitDeclaration(declaration, table) {
36
+ const dispositions = table;
37
+ const engine = {};
38
+ let pieces;
39
+ let seatSeen = false;
40
+ for (const [key, value] of Object.entries(declaration)) {
41
+ if (value === undefined)
42
+ continue;
43
+ const disposition = Object.hasOwn(dispositions, key) ? dispositions[key] : undefined;
44
+ switch (disposition) {
45
+ case "engine":
46
+ engine[key] = value;
47
+ break;
48
+ case "seat": {
49
+ if (seatSeen)
50
+ throw new Error("request declaration: two members are marked as pieces in one disposition table");
51
+ seatSeen = true;
52
+ const record = value;
53
+ if (Object.keys(record).length > 0)
54
+ pieces = record;
55
+ break;
56
+ }
57
+ case "withheld":
58
+ break;
59
+ case undefined:
60
+ throw new Error(`request declaration: a member the disposition table does not classify (${JSON.stringify(key.slice(0, 40))})`);
61
+ default: {
62
+ const unknownDisposition = disposition;
63
+ void unknownDisposition;
64
+ throw new Error("request declaration: a disposition outside the closed set");
65
+ }
66
+ }
67
+ }
68
+ return { engine: engine, ...(pieces === undefined ? {} : { pieces }) };
69
+ }
32
70
  //# sourceMappingURL=plain-string-record.js.map