@sema-agent/server 7.129.0 → 7.130.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 (44) hide show
  1. package/MIGRATION.md +37 -0
  2. package/USAGE.md +38 -2
  3. package/dist/boot/execution-env.js +1 -0
  4. package/dist/boot/resolve-spec.js +8 -4
  5. package/dist/boot/stage-10-http-server.d.ts +1 -1
  6. package/dist/boot/stage-10-http-server.js +2 -0
  7. package/dist/capabilities/mcp-probe.js +16 -3
  8. package/dist/capabilities/scenarios.d.ts +33 -0
  9. package/dist/capabilities/scenarios.js +15 -0
  10. package/dist/capabilities/skills.js +1 -1
  11. package/dist/config-catalog.js +5 -1
  12. package/dist/config-types.d.ts +3 -0
  13. package/dist/config.js +7 -1
  14. package/dist/exec-bounds.d.ts +69 -0
  15. package/dist/exec-bounds.js +37 -0
  16. package/dist/hooks/hook-llm.d.ts +11 -1
  17. package/dist/hooks/hook-llm.js +10 -13
  18. package/dist/hooks/hook-runner.d.ts +1 -1
  19. package/dist/http/routes/capabilities.js +20 -2
  20. package/dist/http/routes/runs.js +12 -4
  21. package/dist/http/server.d.ts +8 -0
  22. package/dist/observability/fail-open.d.ts +5 -1
  23. package/dist/observability/fail-open.js +5 -1
  24. package/dist/plugins/one-time-migrations.d.ts +12 -1
  25. package/dist/plugins/one-time-migrations.js +17 -0
  26. package/dist/plugins/remote-env-adb.js +5 -4
  27. package/dist/plugins/remote-env-e2b.d.ts +31 -9
  28. package/dist/plugins/remote-env-e2b.js +77 -26
  29. package/dist/plugins/remote-env-host.js +7 -5
  30. package/dist/plugins/remote-env-k8s.d.ts +12 -4
  31. package/dist/plugins/remote-env-k8s.js +22 -31
  32. package/dist/plugins/remote-env-local-docker.js +5 -4
  33. package/dist/plugins/remote-env-ssh.js +5 -4
  34. package/dist/runtime-caps-resolver.d.ts +7 -0
  35. package/dist/runtime-caps-resolver.js +1 -0
  36. package/dist/session-titler.d.ts +4 -1
  37. package/dist/session-titler.js +13 -3
  38. package/dist/stored-request-tool-axes-m19.d.ts +29 -0
  39. package/dist/stored-request-tool-axes-m19.js +27 -0
  40. package/dist/task-mcp.d.ts +32 -1
  41. package/dist/task-mcp.js +66 -33
  42. package/dist/tool-axis-words.d.ts +14 -17
  43. package/dist/web-admin-client/skills-mcp.d.ts +1 -1
  44. package/package.json +1 -1
package/dist/task-mcp.js CHANGED
@@ -1,4 +1,4 @@
1
- import { mcpInjectionDroppedNotice, resolveComplianceDenies } from "@sema-agent/core";
1
+ import { mcpInjectionDroppedNotice, readMcpToolAxes, resolveComplianceDenies } from "@sema-agent/core";
2
2
  import { isUsableWireName } from "./wire-name-bound.js";
3
3
  import { HttpError, createCodedHttpError } from "./security.js";
4
4
  import { GOVERNANCE_HTTP_STATUS } from "./governance-http-status.js";
@@ -6,8 +6,8 @@ import { CONTENT_ORIGIN_WORDS, describeBadContentOrigin, isContentOriginWord } f
6
6
  import { readMcpSourceLabel } from "./mcp-source-label.js";
7
7
  import { declaresMcpAssertionHeader, describeMcpAssertionHeaderNotAString, describeMcpIdentityHeadersConflict, mcpIdentityHeadersIssue, noteMcpAssertionHeaderIgnored, noteMcpAssertionHeaderNotSent, readMcpIdentityHeaders, toCoreMcpServerSpec } from "./mcp-assertion-header.js";
8
8
  import { echoKeyName } from "./request-key-closure.js";
9
+ import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
9
10
  import { readMcpToolFaces } from "./mcp-tool-faces.js";
10
- import { isToolAxisIrreversibility, isToolEffect } from "./tool-axis-words.js";
11
11
  export const MAX_REQUEST_MCP_SERVERS = 32;
12
12
  export const MAX_REQUEST_MCP_DROP_NAMES = MAX_REQUEST_MCP_SERVERS;
13
13
  export const MCP_SERVER_ENTRY_KEYS = ["allowTools", "contentOrigin", "elicitation", "name", "source", "toolAxes", "toolFaces", "transport"];
@@ -34,7 +34,7 @@ function declaredName(e) {
34
34
  ? String(e.name)
35
35
  : MALFORMED_NAME;
36
36
  }
37
- function normalizeServer(e, noteBadOrigin) {
37
+ function normalizeServer(e, noteBadOrigin, storedToolAxes) {
38
38
  if (!e || typeof e !== "object")
39
39
  return bad("entry");
40
40
  const o = e;
@@ -76,23 +76,20 @@ function normalizeServer(e, noteBadOrigin) {
76
76
  }
77
77
  if (typeof o.elicitation === "boolean")
78
78
  spec.elicitation = o.elicitation;
79
- if (o.toolAxes && typeof o.toolAxes === "object" && !Array.isArray(o.toolAxes)) {
80
- const axes = {};
81
- for (const [tool, ax] of Object.entries(o.toolAxes)) {
82
- if (!ax || typeof ax !== "object" || Array.isArray(ax))
83
- continue;
84
- const a = ax;
85
- const entry = {
86
- ...(isToolEffect(a.effect) ? { effect: a.effect } : {}),
87
- ...(typeof a.egress === "boolean" ? { egress: a.egress } : {}),
88
- ...(isToolAxisIrreversibility(a.irreversibility) ? { irreversibility: a.irreversibility } : {}),
89
- };
90
- if (Object.keys(entry).length > 0)
91
- axes[tool] = entry;
79
+ const axes = readMcpToolAxes(o.toolAxes);
80
+ let judged = axes;
81
+ let toolAxesReread;
82
+ if (!axes.ok && storedToolAxes !== undefined) {
83
+ const again = readMcpToolAxes(storedToolAxes(o.toolAxes));
84
+ if (again.ok) {
85
+ judged = again;
86
+ toolAxesReread = axes.problem.replace(CONTROL_AND_BIDI_STRIP_RE, "");
92
87
  }
93
- if (Object.keys(axes).length > 0)
94
- spec.toolAxes = axes;
95
88
  }
89
+ if (!judged.ok)
90
+ return bad("toolAxes");
91
+ if (judged.toolAxes !== undefined)
92
+ spec.toolAxes = judged.toolAxes;
96
93
  if (o.contentOrigin !== undefined) {
97
94
  if (isContentOriginWord(o.contentOrigin))
98
95
  spec.contentOrigin = o.contentOrigin;
@@ -105,29 +102,32 @@ function normalizeServer(e, noteBadOrigin) {
105
102
  return bad("toolFaces");
106
103
  if (faces.faces !== undefined)
107
104
  spec.toolFaces = faces.faces;
108
- return { ok: true, spec };
105
+ return { ok: true, spec, ...(toolAxesReread !== undefined ? { toolAxesReread } : {}) };
109
106
  }
110
107
  export function validateRequestMcp(raw) {
111
- if (raw === undefined || raw === null)
112
- return validateRequestMcpEntries([]);
113
- if (!Array.isArray(raw))
108
+ if (raw !== undefined && raw !== null && !Array.isArray(raw))
114
109
  return null;
115
- return validateRequestMcpEntries(raw);
110
+ const { ok, dropped, droppedTotal, badContentOrigin } = validateRequestMcpEntries(Array.isArray(raw) ? raw : []);
111
+ return { ok, dropped, droppedTotal, badContentOrigin };
116
112
  }
117
- function validateRequestMcpEntries(raw) {
113
+ function validateRequestMcpEntries(raw, storedToolAxes) {
118
114
  const ok = [];
119
115
  const dropped = [];
120
116
  const badContentOrigin = [];
117
+ const toolAxesReread = [];
121
118
  for (const e of raw.slice(0, MAX_REQUEST_MCP_SERVERS)) {
122
- const v = normalizeServer(e, (server, value) => badContentOrigin.push({ server, value: describeBadContentOrigin(value) }));
123
- if (v.ok)
119
+ const v = normalizeServer(e, (server, value) => badContentOrigin.push({ server, value: describeBadContentOrigin(value) }), storedToolAxes);
120
+ if (v.ok) {
124
121
  ok.push(v.spec);
122
+ if (v.toolAxesReread !== undefined)
123
+ toolAxesReread.push({ server: v.spec.name, problem: v.toolAxesReread });
124
+ }
125
125
  else
126
126
  dropped.push({ server: declaredName(e), reason: "malformed_entry", field: v.field });
127
127
  }
128
128
  for (const e of raw.slice(MAX_REQUEST_MCP_SERVERS, MAX_REQUEST_MCP_SERVERS + MAX_REQUEST_MCP_DROP_NAMES))
129
129
  dropped.push({ server: declaredName(e), reason: "over_cap" });
130
- return { ok, dropped, droppedTotal: Math.max(0, raw.length - ok.length), badContentOrigin };
130
+ return { ok, dropped, droppedTotal: Math.max(0, raw.length - ok.length), badContentOrigin, toolAxesReread };
131
131
  }
132
132
  export function assertRequestMcpUnlocked(bodyMcp, lockedKeys) {
133
133
  if (!lockedKeys.has("mcp"))
@@ -180,6 +180,29 @@ export function requestEntryIdentityRefusal(entry) {
180
180
  const value = (entry.transport).assertionHeader;
181
181
  return { errorCode: "request.field_invalid", message: describeMcpAssertionHeaderNotAString(name, value) };
182
182
  }
183
+ export function assertRequestMcpToolAxes(bodyMcp) {
184
+ if (!Array.isArray(bodyMcp))
185
+ return;
186
+ for (let i = 0; i < bodyMcp.length; i++) {
187
+ const refusal = requestEntryToolAxesRefusal(bodyMcp[i], `mcpServers[${i}]`);
188
+ if (refusal !== undefined)
189
+ throw createCodedHttpError(400, refusal.errorCode, refusal.message);
190
+ }
191
+ }
192
+ export function requestEntryToolAxesRefusal(entry, at) {
193
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
194
+ return undefined;
195
+ const read = readMcpToolAxes(entry.toolAxes);
196
+ if (read.ok)
197
+ return undefined;
198
+ const server = echoKeyName("", declaredName(entry));
199
+ return {
200
+ errorCode: "request.field_invalid",
201
+ message: `${at}.toolAxes (server "${server}") is not a usable declaration — ${read.problem.replace(CONTROL_AND_BIDI_STRIP_RE, "")}. ` +
202
+ `The whole declaration is refused: the tools that were fine are not kept and nothing is defaulted, because a toolAxes declaration is your own risk-axis judgement for this server's tools ` +
203
+ `(it can move a tool down or up), and a half-applied one would silently differ from what you declared. Fix the entry, or omit the key to declare nothing.`,
204
+ };
205
+ }
183
206
  export function deploymentClosedMcpNames(baseline, reservedNames) {
184
207
  return new Set([...(baseline ?? []).map((s) => s.name), ...reservedNames]);
185
208
  }
@@ -190,20 +213,20 @@ export function mergeRequestMcp(baseline, gated, reservedNames) {
190
213
  const add = gated.filter((s) => !closed.has(s.name));
191
214
  return add.length === 0 ? baseline : [...(baseline ?? []), ...add];
192
215
  }
193
- export function resolveRequestMcp(baseline, bodyMcp, config, logger, notice) {
216
+ export function resolveRequestMcp(baseline, bodyMcp, config, logger, notice, storedToolAxes) {
194
217
  if (bodyMcp === undefined || bodyMcp === null)
195
218
  return baseline;
196
- const { mcp, drops } = classifyRequestMcp(baseline, bodyMcp, config, logger);
219
+ const { mcp, drops } = classifyRequestMcp(baseline, bodyMcp, config, logger, storedToolAxes);
197
220
  mintInjectionDropNotices(drops, notice);
198
221
  return mcp;
199
222
  }
200
- function classifyRequestMcp(baseline, bodyMcp, config, logger) {
223
+ function classifyRequestMcp(baseline, bodyMcp, config, logger, storedToolAxes) {
201
224
  if (bodyMcp !== undefined && bodyMcp !== null && !Array.isArray(bodyMcp)) {
202
225
  logger?.warn("mcp_injection_ignored", { reason: "mcpServers not an array" });
203
226
  return { mcp: baseline, drops: [] };
204
227
  }
205
228
  const entries = Array.isArray(bodyMcp) ? bodyMcp : [];
206
- const outcome = mcpInjectionHonored(config) ? classifyHonored(baseline, entries, config, logger) : classifyGateClosed(baseline, entries);
229
+ const outcome = mcpInjectionHonored(config) ? classifyHonored(baseline, entries, config, logger, storedToolAxes) : classifyGateClosed(baseline, entries);
207
230
  if (outcome.drops.length > 0)
208
231
  logger?.warn("mcp_injection_dropped", { dropped: outcome.drops, droppedTotal: outcome.droppedTotal });
209
232
  return { mcp: outcome.mcp, drops: outcome.drops };
@@ -215,8 +238,8 @@ function classifyGateClosed(baseline, entries) {
215
238
  droppedTotal: entries.length,
216
239
  };
217
240
  }
218
- function classifyHonored(baseline, entries, config, logger) {
219
- const v = validateRequestMcpEntries(entries);
241
+ function classifyHonored(baseline, entries, config, logger, storedToolAxes) {
242
+ const v = validateRequestMcpEntries(entries, storedToolAxes);
220
243
  if (v.badContentOrigin.length > 0) {
221
244
  logger?.warn("mcp_content_class_dropped", {
222
245
  entries: v.badContentOrigin,
@@ -236,6 +259,16 @@ function classifyHonored(baseline, entries, config, logger) {
236
259
  if (issue !== undefined && mounted.has(declaredName(e)))
237
260
  noteMcpAssertionHeaderIgnored(logger, declaredName(e), issue);
238
261
  }
262
+ for (const r of v.toolAxesReread) {
263
+ if (!mounted.has(r.server))
264
+ continue;
265
+ logger?.warn("mcp_tool_axes_read_as_before_upgrade", {
266
+ server: r.server,
267
+ migration: "M19",
268
+ problem: r.problem,
269
+ note: "this replayed request was stored before the upgrade and its toolAxes declaration does not pass the engine's reader; while one-time migration M19 is open it is read the way the pre-upgrade server read it (members that did not fit are dropped, a declaration that is not an object counts as none) and the server stays mounted — once the window closes the same request leaves this server unmounted",
270
+ });
271
+ }
239
272
  return {
240
273
  mcp: mergeRequestMcp(baseline, v.ok.map(toCoreMcpServerSpec), reservedNames),
241
274
  drops: [...v.dropped, ...blocked.map((server) => ({ server, reason: "name_reserved_by_deployment" }))],
@@ -1,30 +1,27 @@
1
1
  /**
2
- * S-249(DT-13)—— per-tool **风险轴覆写**的值域词表与谓词,**两面共用一份**。
2
+ * S-249(DT-13)—— per-tool **风险轴覆写**的值域词表与谓词(读者:**A2A 请求腿**;以及一次性迁移 M19 的升级前读法 `stored-request-tool-axes-m19.ts`,随 M19 的登记行退役)。
3
3
  *
4
- * ## 病(修前)
5
- * `task-mcp.ts` 与 `task-a2a.ts` 各自在请求腿上逐键校验同一个三成员结构
6
- * (`effect` / `egress` / `irreversibility`),而两个**闭集词表**是**手抄的字面串**:四份字面量
7
- * (两文件 × 两个词轴),没有一份钉在 core 的型上。后果不是「今天判错了」,而是**判据比声明窄**这件事
8
- * 没有任何门看得见 —— core 哪天给 `ToolEffect` 加一个词(或把 `irreversibility` 的两词改成三词,它在
9
- * `AskOrigin` 那一面**已经**是 `never|maybe|always` 三词了),两条请求腿会把那个**合法**的新词
10
- * **静默剔除**(MCP 腿)或**整条 server 判假**(A2A 腿),而 tsc 一声不吭。
4
+ * ## 历史与现状
5
+ * 修前 `task-mcp.ts` 与 `task-a2a.ts` 各自在请求腿上逐键校验同一个三成员结构(`effect` / `egress` / `irreversibility`),两个**闭集词表**是手抄的字面串,
6
+ * 没有一份钉在 core 的型上;S-249 把词表收到本文件并**两向钉死在 core 的型上**(core 加词 ⇒ 正向门红;本表留了 core 已没有的词 ⇒ 反向门红),两条腿读同一只谓词。
11
7
  *
12
- * ## 修法
13
- * 词表留在本仓(运行期要它),但**两向钉死在 core 的型上**:core 加词 ⇒ 正向门红;本表留了一个 core
14
- * 已经没有的词 ⇒ 反向门红。两个面共用**同一只**谓词,于是「什么算一个合法的轴值」在本仓只有一个答案。
8
+ * 之后 MCP 面的 `toolAxes` 判官整个换给了**引擎**:core 7.51.2 起根导出 `readMcpToolAxes`(整份声明一坏全拒、不默认),部署腿(`web-admin-client/skills-mcp.ts`)
9
+ * 与请求腿(`task-mcp.ts`)都问它,本仓不再有 MCP 面的自铸读者、也不再 import 本文件的谓词。**A2A 面引擎没有对应的判官**(core 暂无 `readA2aToolAxes`),
10
+ * 所以 A2A 请求腿(`task-a2a.ts`)仍用本文件的谓词;引擎一旦给 A2A 也立一只判官,本文件随之退役。另一个读者是 M19 的迁移读者:续跑重放腿把升级前存下的
11
+ * `mcpServers[].toolAxes` 按升级前那只读者的读法重读一次(产物再交引擎的判官),那段读法当年就读本文件的谓词,逐字搬回;它不是 MCP 面的第二只判官。
15
12
  *
16
- * 🔴 **只管值域,不管处置**:两条腿对坏值的处置**刻意不同**(MCP = 丢这一键、留 server;A2A = 整条拒),
17
- * 判据与理由各写在各自的调用点。合并处置会抹掉那条真实的分歧(MCP 的 `toolAxes` 有 server-hint 腿可折,
18
- * A2A 收下的就是最终值),而分歧本身是裁定过的。
13
+ * 下面的同型钉(A2A 的轴型与 MCP 的轴型逐字同声明)仍有意义:A2A 的词表跟着引擎 MCP 面的轴型走,哪一边加词、改词,另一边在这里当场红。
14
+ *
15
+ * 🔴 **只管值域,不管处置**:A2A 腿对坏值的处置是整条拒(MCP 面是引擎判官的「整份全拒」;两面方向一致),M19 的迁移读者剔坏成员(升级前的读法),判据与理由写在各自的调用点。
19
16
  */
20
17
  import type { McpServerSpec, ToolEffect } from "@sema-agent/core";
21
- /** 一条 per-tool 轴覆写的形。取 MCP 那一面作代表 —— 下面的同型钉证明两面逐字同声明。 */
18
+ /** 一条 per-tool 轴覆写的形。取 MCP 那一面作代表 —— 下面的同型钉证明 A2A 面与它逐字同声明。 */
22
19
  export type ToolAxisEntry = NonNullable<McpServerSpec["toolAxes"]>[string];
23
20
  /** 轴覆写上那个**两词**的不可逆档(⚠️ 与 `AskOrigin` 面的**三词** `never|maybe|always` 不是同一个词表:
24
21
  * 覆写面只收两极,`maybe` 是引擎按工具自己的声明定的,调用方说不出口)。 */
25
22
  export type ToolAxisIrreversibility = NonNullable<ToolAxisEntry["irreversibility"]>;
26
- /** 值是不是一个合法的 `effect` 词(两条请求腿共用)。 */
23
+ /** 值是不是一个合法的 `effect` 词(A2A 请求腿与 M19 的迁移读者读它)。 */
27
24
  export declare function isToolEffect(v: unknown): v is ToolEffect;
28
- /** 值是不是一个合法的**覆写面** `irreversibility` 词(两条请求腿共用)。 */
25
+ /** 值是不是一个合法的**覆写面** `irreversibility` 词(A2A 请求腿与 M19 的迁移读者读它)。 */
29
26
  export declare function isToolAxisIrreversibility(v: unknown): v is ToolAxisIrreversibility;
30
27
  //# sourceMappingURL=tool-axis-words.d.ts.map
@@ -27,7 +27,7 @@ export declare function applySkillsManifest(baseline: LoadedSkill[], manifest: {
27
27
  * Resolve center MCP servers (`/effective.mcp`) into core `McpServerSpec`s + the scenarios each applies to.
28
28
  * env-NAME refs → real values from THIS service's env (secret boundary unchanged); a server with any missing
29
29
  * ref is skipped (`resolveRefs` → null). `enabled:false` skipped. A server whose `toolFaces` / `toolAxes` declaration is
30
- * malformed is skipped whole (both are trust-root declarations — see `mcp-tool-faces.ts` / `mcp-tool-axes.ts`). Restart-to-apply
30
+ * malformed is skipped whole (both are trust-root declarations — see `mcp-tool-faces.ts` / the engine's `readMcpToolAxes`). Restart-to-apply
31
31
  * (filtered per scenario in `resolveSpec` via `mcpForScenario`).
32
32
  */
33
33
  export declare function resolveMcpServers(mcp: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.129.0",
3
+ "version": "7.130.0",
4
4
  "description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",