@sema-agent/server 7.127.2 → 7.128.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.
package/dist/task-mcp.js CHANGED
@@ -1,16 +1,18 @@
1
1
  import { mcpInjectionDroppedNotice, resolveComplianceDenies } from "@sema-agent/core";
2
2
  import { isUsableWireName } from "./wire-name-bound.js";
3
- import { HttpError } from "./security.js";
3
+ import { HttpError, createCodedHttpError } from "./security.js";
4
4
  import { GOVERNANCE_HTTP_STATUS } from "./governance-http-status.js";
5
5
  import { CONTENT_ORIGIN_WORDS, describeBadContentOrigin, isContentOriginWord } from "./mcp-content-origin.js";
6
6
  import { readMcpSourceLabel } from "./mcp-source-label.js";
7
+ import { declaresMcpAssertionHeader, describeMcpAssertionHeaderNotAString, describeMcpIdentityHeadersConflict, mcpIdentityHeadersIssue, noteMcpAssertionHeaderIgnored, noteMcpAssertionHeaderNotSent, readMcpIdentityHeaders, toCoreMcpServerSpec } from "./mcp-assertion-header.js";
8
+ import { echoKeyName } from "./request-key-closure.js";
7
9
  import { readMcpToolFaces } from "./mcp-tool-faces.js";
8
10
  import { isToolAxisIrreversibility, isToolEffect } from "./tool-axis-words.js";
9
11
  export const MAX_REQUEST_MCP_SERVERS = 32;
10
12
  export const MAX_REQUEST_MCP_DROP_NAMES = MAX_REQUEST_MCP_SERVERS;
11
13
  export const MCP_SERVER_ENTRY_KEYS = ["allowTools", "contentOrigin", "elicitation", "name", "source", "toolAxes", "toolFaces", "transport"];
12
14
  export const STDIO_MCP_TRANSPORT_KEYS = ["args", "command", "env", "kind"];
13
- export const HTTP_MCP_TRANSPORT_KEYS = ["headers", "kind", "principalHeader", "url"];
15
+ export const HTTP_MCP_TRANSPORT_KEYS = ["assertionHeader", "headers", "kind", "principalHeader", "url"];
14
16
  export const MCP_ENTRY_EMPTY_RECORD_KEYS = ["toolAxes", "toolFaces"];
15
17
  export const MCP_ENTRY_EMPTY_ARRAY_KEYS = ["allowTools"];
16
18
  export const MCP_TRANSPORT_EMPTY_RECORD_KEYS = ["env", "headers"];
@@ -60,7 +62,7 @@ function normalizeServer(e, noteBadOrigin) {
60
62
  kind: "http",
61
63
  url: tr.url,
62
64
  ...(isStringRecord(tr.headers) ? { headers: tr.headers } : {}),
63
- ...(typeof tr.principalHeader === "string" ? { principalHeader: tr.principalHeader } : {}),
65
+ ...readMcpIdentityHeaders(tr),
64
66
  };
65
67
  }
66
68
  else {
@@ -151,6 +153,33 @@ export function assertRequestMcpContentOrigin(bodyMcp) {
151
153
  ` (omit the key entirely for the default: this server's tools classify as external content)`, { code: "request.field_invalid" });
152
154
  }
153
155
  }
156
+ export function assertRequestMcpIdentityHeaders(bodyMcp) {
157
+ if (!Array.isArray(bodyMcp))
158
+ return;
159
+ for (const e of bodyMcp) {
160
+ const refusal = requestEntryIdentityRefusal(e);
161
+ if (refusal !== undefined)
162
+ throw createCodedHttpError(400, refusal.errorCode, refusal.message);
163
+ }
164
+ }
165
+ export function requestEntryIdentityIssue(entry) {
166
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
167
+ return undefined;
168
+ const t = entry.transport;
169
+ if (!t || typeof t !== "object" || Array.isArray(t))
170
+ return undefined;
171
+ return mcpIdentityHeadersIssue(t);
172
+ }
173
+ export function requestEntryIdentityRefusal(entry) {
174
+ const issue = requestEntryIdentityIssue(entry);
175
+ if (issue === undefined)
176
+ return undefined;
177
+ const name = echoKeyName("", declaredName(entry));
178
+ if (issue === "both_set")
179
+ return { errorCode: "request.body_shape", message: describeMcpIdentityHeadersConflict(name) };
180
+ const value = (entry.transport).assertionHeader;
181
+ return { errorCode: "request.field_invalid", message: describeMcpAssertionHeaderNotAString(name, value) };
182
+ }
154
183
  export function deploymentClosedMcpNames(baseline, reservedNames) {
155
184
  return new Set([...(baseline ?? []).map((s) => s.name), ...reservedNames]);
156
185
  }
@@ -198,8 +227,17 @@ function classifyHonored(baseline, entries, config, logger) {
198
227
  const reservedNames = config.mcpServers?.reservedNames ?? [];
199
228
  const closed = deploymentClosedMcpNames(baseline, reservedNames);
200
229
  const blocked = v.ok.filter((s) => closed.has(s.name)).map((s) => s.name);
230
+ for (const s of v.ok)
231
+ if (declaresMcpAssertionHeader(s) && !closed.has(s.name))
232
+ noteMcpAssertionHeaderNotSent(logger, s.name);
233
+ const mounted = new Set(v.ok.filter((s) => !closed.has(s.name)).map((s) => s.name));
234
+ for (const e of entries.slice(0, MAX_REQUEST_MCP_SERVERS)) {
235
+ const issue = requestEntryIdentityIssue(e);
236
+ if (issue !== undefined && mounted.has(declaredName(e)))
237
+ noteMcpAssertionHeaderIgnored(logger, declaredName(e), issue);
238
+ }
201
239
  return {
202
- mcp: mergeRequestMcp(baseline, v.ok, reservedNames),
240
+ mcp: mergeRequestMcp(baseline, v.ok.map(toCoreMcpServerSpec), reservedNames),
203
241
  drops: [...v.dropped, ...blocked.map((server) => ({ server, reason: "name_reserved_by_deployment" }))],
204
242
  droppedTotal: v.droppedTotal + blocked.length,
205
243
  };
@@ -289,7 +289,7 @@ export function toolEndEventData(ev) {
289
289
  ...(ev.output !== undefined ? { output: projectToolOutput(ev.output) } : {}),
290
290
  ...(ev.truncated ? { truncated: true } : {}),
291
291
  ...(Number.isFinite(ev.totalChars) ? { totalChars: ev.totalChars } : {}),
292
- ...(ev.structured !== undefined ? { structured: projectToolStructured(ev.structured) } : {}),
292
+ ...(ev.structured !== undefined ? { structured: projectToolStructured(ev.structured, redactDeep) } : {}),
293
293
  ...(typeof ev.errorCode === "string" && ev.errorCode.length <= 128 ? { errorCode: ev.errorCode } : {}),
294
294
  ...(typeof ev.delivered === "string" && MCP_DELIVERY_VERDICTS.includes(ev.delivered) ? { delivered: ev.delivered } : {}),
295
295
  ...(gate !== undefined ? { gate } : {}),
@@ -4,10 +4,15 @@
4
4
  */
5
5
  export declare function projectToolOutput(output: unknown): unknown;
6
6
  /**
7
- * 两面共用的 `structured` 投影(与 {@link projectToolOutput} 同律):媒体卡逐成员过 `redactDeep`,`file` 里 `base64` 逐字、其余成员照脱;
8
- * 其它一切整只过 `redactDeep`。保键序(逐键映射),不改入参。
7
+ * 媒体卡座位的**唯一一只遍历**(与 {@link projectToolOutput} 同律):媒体卡逐成员过 `redact`,`file` 里 `base64` 逐字、其余成员照脱;
8
+ * 其它一切整只过 `redact`。保键序(逐键映射),不改入参。
9
+ *
10
+ * 形判与遍历只此一只,**脱敏器由调用面给**(「什么算该脱」是各面自己的事,「字节座位不过它」是这里的事):
11
+ * · `tool_end` 活面 / 持久面(`toolEndEventData` 经 `trace/project.ts`):凭据脱敏 `redactDeep`;
12
+ * · 会话存档读面(`audit.ts` 的 `toolResult.details` 叶):模型文本段脱敏的深扫(`redactDeepWith(v, redactModelText)`)。
13
+ * `redact` 必须是整值→整值的纯函数(环 / 深度守卫在它里面,遍历不重做);非媒体卡原样交给它,所以非媒体卡的读数就是调用面自己的深扫。
9
14
  */
10
- export declare function projectToolStructured(structured: unknown): unknown;
15
+ export declare function projectToolStructured(structured: unknown, redact: (value: unknown) => unknown): unknown;
11
16
  /**
12
17
  * `tool_end` 载荷的媒体降级(持久面恒用;活面只在连接积压时用),**一次调用降两个座位**:
13
18
  * · `output`:带 `data` 的媒体块换成描述符,有任何一块被换 ⇒ 帧级 `truncated: true` 且帧级 `totalChars` 在场 —— core 不变量
@@ -21,11 +21,11 @@ function isMediaCard(s) {
21
21
  const file = card.file;
22
22
  return file !== null && typeof file === "object" && !Array.isArray(file) && typeof file.base64 === "string";
23
23
  }
24
- export function projectToolStructured(structured) {
24
+ export function projectToolStructured(structured, redact) {
25
25
  if (!isMediaCard(structured))
26
- return redactDeep(structured);
27
- const file = Object.fromEntries(Object.entries(structured.file).map(([k, v]) => [k, k === "base64" ? v : redactDeep(v)]));
28
- return Object.fromEntries(Object.entries(structured).map(([k, v]) => [k, k === "file" ? file : redactDeep(v)]));
26
+ return redact(structured);
27
+ const file = Object.fromEntries(Object.entries(structured.file).map(([k, v]) => [k, k === "base64" ? v : redact(v)]));
28
+ return Object.fromEntries(Object.entries(structured).map(([k, v]) => [k, k === "file" ? file : redact(v)]));
29
29
  }
30
30
  function trueSizeOf(b) {
31
31
  if (isMediaBlock(b))
@@ -50,7 +50,7 @@
50
50
  export { fetchEffective, fetchPrincipalCaps, fetchPrincipalOrgMemory, WebAdminHttpError, fetchSkillContent, fetchPromptArtifact, fetchPromptBlob, } from "./http-client.js";
51
51
  export { mutateInPlace, applyEffective, type ApplyReport, applyRuntimeGates, applyRuntimeHot, logEffectiveDiff, MODEL_PLANE_DOMAINS, type ModelPlaneDomain, type ModelPlaneOf, withModelPlaneOf, fullyLandedForLkg, } from "./apply-effective.js";
52
52
  export { restartReasons, modelPlaneChanged, type RestartSlice, type RestartSliceCtx, type EngineRestartSignal, } from "./restart-signal.js";
53
- export { applyCenterSkills, resolveMcpServers, mcpForScenario, resolveA2aPeers, a2aForScenario } from "./skills-mcp.js";
53
+ export { applySkillsManifest, resolveMcpServers, mcpForScenario, resolveA2aPeers, a2aForScenario } from "./skills-mcp.js";
54
54
  export { applyCenterReadFace, CENTER_READ_FACE_LIVE_KEYS, type CenterReadFaceApply, type CenterReadFaceReport, type ReadFaceBaseline } from "./read-face.js";
55
55
  export type { CenterSkillManifest, CenterMcpServer, CenterA2aPeer, CenterModel, CenterReadFace, EffectiveConfig, ExecutionRuling, SessionMirrorRuling, } from "./types.js";
56
56
  //# sourceMappingURL=facade.d.ts.map
@@ -1,6 +1,6 @@
1
1
  export { fetchEffective, fetchPrincipalCaps, fetchPrincipalOrgMemory, WebAdminHttpError, fetchSkillContent, fetchPromptArtifact, fetchPromptBlob, } from "./http-client.js";
2
2
  export { mutateInPlace, applyEffective, applyRuntimeGates, applyRuntimeHot, logEffectiveDiff, MODEL_PLANE_DOMAINS, withModelPlaneOf, fullyLandedForLkg, } from "./apply-effective.js";
3
3
  export { restartReasons, modelPlaneChanged, } from "./restart-signal.js";
4
- export { applyCenterSkills, resolveMcpServers, mcpForScenario, resolveA2aPeers, a2aForScenario } from "./skills-mcp.js";
4
+ export { applySkillsManifest, resolveMcpServers, mcpForScenario, resolveA2aPeers, a2aForScenario } from "./skills-mcp.js";
5
5
  export { applyCenterReadFace, CENTER_READ_FACE_LIVE_KEYS } from "./read-face.js";
6
6
  //# sourceMappingURL=facade.js.map
@@ -88,8 +88,8 @@ export declare class WebAdminHttpError extends Error {
88
88
  * sha256 matches the manifest hash (integrity + rewrite check). The content is immutable per hash, so the
89
89
  * caller caches by hash and never re-fetches an unchanged skill. Throws a {@link WebAdminHttpError}
90
90
  * (carrying `.status`) on an HTTP error — INCLUDING 404 — or a plain Error on hash mismatch. Callers that
91
- * want "unknown hash → undefined" parity with the local backend inspect `.status === 404` and swallow it;
92
- * the legacy `applyCenterSkills` path keeps treating every throw as "skip this skill, keep baseline".
91
+ * want "unknown hash → undefined" parity with the local backend inspect `.status === 404` and swallow it —
92
+ * `RemoteConfigProvider.fetchSkillContent` does exactly that, and it is the only caller on the skills read port.
93
93
  */
94
94
  export declare function fetchSkillContent(center: CenterEndpoint, contentHash: string, fetchImpl?: typeof fetch): Promise<string>;
95
95
  /** [ref]b / [ref]① — §9.4 by-digest 补拉二函(fetchSkillContent 同姿势:HTTP+Bearer+8s 超时+
@@ -2,30 +2,33 @@ import type { A2aServerSpec, McpServerSpec } from "@sema-agent/core";
2
2
  import type { CenterMcp, ScopedA2aPeer } from "../config-types.js";
3
3
  import type { Logger } from "../observability/logger.js";
4
4
  import type { LoadedSkill } from "../capabilities/skills.js";
5
- import type { CenterEndpoint } from "./center-request.js";
5
+ import type { ConfigProvider } from "../config-provider.js";
6
6
  import type { CenterA2aPeer, CenterMcpServer, CenterSkillManifest } from "./types.js";
7
7
  /**
8
- * Merge center skills OVER the image baseline ([ref]-sibling B1; `web-admin docs/MCP-SKILLS.md`). Load order =
9
- * `loadSkills(SKILLS_DIR)` baseline → center skills lazily fetched by hash and overlaid BY NAME (center
10
- * wins on a clash → can override a built-in; guarded center-side by publisher RBAC + the publish gate).
11
- * `enabled:false` is skipped. Content is verified + cached by hash (same hash fetched once). A skill whose
12
- * content fetch/verify fails is skipped (the baseline version, if any, stays) — never blocks startup.
8
+ * 技能正文的来源 = **拉到这份清单的配置源自己**(S-1556):`fetchSkillContent(contentHash)` 按 hash 答正文 —— 远程 = 中心内容端点
9
+ * (传输自验 hash;未知 hash ⇒ `undefined`),本地 = config.d 里内联的正文(与清单同一种容错读);`skillBodyCacheDir` = 这只配置源
10
+ * 声明的正文盘缓存目录(远程且开着 LKG 才有;本地恒缺席 —— 正文本就在这块盘上)。本函数不知道、也不问是哪一种。
11
+ */
12
+ export type SkillBodySource = Pick<ConfigProvider, "fetchSkillContent" | "skillBodyCacheDir">;
13
+ /**
14
+ * The skills domain's ONE read port (S-1556; [ref]-sibling B1; `web-admin docs/MCP-SKILLS.md`). Load order =
15
+ * `loadSkills(SKILLS_DIR)` baseline → the config source's skills (web-admin center OR local `config.d` — same manifest
16
+ * shape, same code path) overlaid BY NAME (config wins on a clash → can override a built-in; guarded center-side by
17
+ * publisher RBAC + the publish gate). `enabled:false` is skipped and never fetched. Bodies come from `bodies` by content
18
+ * hash, deduped in memory (same hash read once). A skill whose body is absent at the source or fails to read (incl. the
19
+ * remote transport's hash check) is NOT installed — exactly one `web_admin_skill_failed` warn names it (`reason`:
20
+ * `body_absent` | `read_failed`), the baseline of that name (if any) stays, and boot is never blocked.
13
21
  * Restart-to-apply: this runs at boot, before `buildScenarios` bakes skills into scenarios.
14
22
  */
15
- export declare function applyCenterSkills(baseline: LoadedSkill[], manifest: {
23
+ export declare function applySkillsManifest(baseline: LoadedSkill[], manifest: {
16
24
  skills: CenterSkillManifest[];
17
- },
18
- /** S-651:中心端点(基址 + 凭证来源;每次正文拉取取一次值)。 */
19
- center: CenterEndpoint, logger?: Logger, fetchImpl?: typeof fetch,
20
- /** LKG 姊妹面(clay 拍 2026-07-18):skill 正文的 content-addressed 盘缓存目录。读序=内存→盘(验
21
- * hash,坏文件忽略)→中心(成功后 best-effort 回写)。让 LKG-boot 在中心不可达时不再于正文拉取上
22
- * 挂数秒(live 实测黑洞 +5.3s/skill 批),顺带省正常 refresh 的重复拉取。undefined=无盘缓存(原行为)。 */
23
- diskCacheDir?: string): Promise<LoadedSkill[]>;
25
+ }, bodies: SkillBodySource, logger?: Logger): Promise<LoadedSkill[]>;
24
26
  /**
25
27
  * Resolve center MCP servers (`/effective.mcp`) into core `McpServerSpec`s + the scenarios each applies to.
26
28
  * env-NAME refs → real values from THIS service's env (secret boundary unchanged); a server with any missing
27
- * ref is skipped (`resolveRefs` → null). `enabled:false` skipped. Restart-to-apply (filtered per scenario in
28
- * `resolveSpec` via `mcpForScenario`).
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
31
+ * (filtered per scenario in `resolveSpec` via `mcpForScenario`).
29
32
  */
30
33
  export declare function resolveMcpServers(mcp: {
31
34
  servers: CenterMcpServer[];
@@ -1,11 +1,13 @@
1
1
  import { readMcpSourceLabel } from "../mcp-source-label.js";
2
2
  import { readMcpToolFaces } from "../mcp-tool-faces.js";
3
+ import { readMcpToolAxes } from "../mcp-tool-axes.js";
3
4
  import { skillContentHash } from "@sema-agent/settings-schema";
4
5
  import { CONTENT_ORIGIN_WORDS, describeBadContentOrigin, isContentOriginWord } from "../mcp-content-origin.js";
5
- import { fetchSkillContent } from "./http-client.js";
6
+ import { noteMcpAssertionHeaderNotSent } from "../mcp-assertion-header.js";
6
7
  import { SHA256_HEX_RE } from "../digest-form.js";
7
8
  import { describeBadWireName, isUsableWireName } from "../wire-name-bound.js";
8
- export async function applyCenterSkills(baseline, manifest, center, logger, fetchImpl = fetch, diskCacheDir) {
9
+ export async function applySkillsManifest(baseline, manifest, bodies, logger) {
10
+ const diskCacheDir = bodies.skillBodyCacheDir;
9
11
  const { promises: fsp } = await import("node:fs");
10
12
  const { join: joinPath } = await import("node:path");
11
13
  const diskRead = async (hash) => {
@@ -18,7 +20,7 @@ export async function applyCenterSkills(baseline, manifest, center, logger, fetc
18
20
  const text = await fsp.readFile(joinPath(diskCacheDir, hex), "utf8");
19
21
  if (skillContentHash(text) === hash)
20
22
  return text;
21
- logger?.warn("web_admin_skill_cache_corrupt", { hash, note: "on-disk body fails its own hash — ignored, refetching from center" });
23
+ logger?.warn("web_admin_skill_cache_corrupt", { hash, note: "on-disk body fails its own hash — ignored, re-reading from the config source" });
22
24
  }
23
25
  catch {
24
26
  }
@@ -43,14 +45,19 @@ export async function applyCenterSkills(baseline, manifest, center, logger, fetc
43
45
  if (m.enabled === false)
44
46
  continue;
45
47
  if (seenNames.has(m.name))
46
- logger?.warn("web_admin_skill_duplicate_name", { name: m.name, note: "duplicate name in center manifest — last entry wins" });
48
+ logger?.warn("web_admin_skill_duplicate_name", { name: m.name, note: "duplicate name in the skills manifest — last entry wins" });
47
49
  seenNames.add(m.name);
48
50
  try {
49
51
  let content = contentCache.get(m.contentHash);
50
52
  if (content === undefined)
51
53
  content = await diskRead(m.contentHash);
52
54
  if (content === undefined) {
53
- content = await fetchSkillContent(center, m.contentHash, fetchImpl);
55
+ const answered = await bodies.fetchSkillContent(m.contentHash);
56
+ if (answered === undefined) {
57
+ logger?.warn("web_admin_skill_failed", { name: m.name, hash: m.contentHash, reason: "body_absent", note: "the config source holds no body for this hash — this entry is NOT installed; the baseline skill of this name (if any) stays" });
58
+ continue;
59
+ }
60
+ content = answered;
54
61
  diskWrite(m.contentHash, content);
55
62
  }
56
63
  contentCache.set(m.contentHash, content);
@@ -59,7 +66,7 @@ export async function applyCenterSkills(baseline, manifest, center, logger, fetc
59
66
  logger?.info("web_admin_skill", { name: m.name, hash: m.contentHash, scenarios: m.scenarios, overrodeBuiltin });
60
67
  }
61
68
  catch (err) {
62
- logger?.warn("web_admin_skill_failed", { name: m.name, hash: m.contentHash, err: String(err), note: "keeping baseline if any; skipping center version" });
69
+ logger?.warn("web_admin_skill_failed", { name: m.name, hash: m.contentHash, reason: "read_failed", err: String(err), note: "reading the body from the config source failed — this entry is NOT installed; the baseline skill of this name (if any) stays" });
63
70
  }
64
71
  }
65
72
  await Promise.all(pendingWrites);
@@ -117,22 +124,34 @@ export function resolveMcpServers(mcp, logger) {
117
124
  continue;
118
125
  }
119
126
  const toolFaces = facesRead.faces === undefined ? {} : { toolFaces: facesRead.faces };
127
+ const axesRead = readMcpToolAxes(s.toolAxes);
128
+ if (!axesRead.ok) {
129
+ logger?.warn("web_admin_mcp_tool_axes_invalid", {
130
+ server: s.name,
131
+ problem: axesRead.problem,
132
+ note: "server SKIPPED — a toolAxes declaration is a TRUST-ROOT declaration (it can lower an effect so a call skips the approval fence, or raise one); mounting the server without it, or with a half of it, would silently change what you declared. Fix the entry and republish.",
133
+ });
134
+ continue;
135
+ }
136
+ const toolAxes = axesRead.toolAxes === undefined ? {} : { toolAxes: axesRead.toolAxes };
120
137
  if (s.transport.kind === "stdio") {
121
138
  const env = resolveRefs(s.transport.envRefs, s.name, "env", logger);
122
139
  if (env === null)
123
140
  continue;
124
141
  out.push({
125
142
  scenarios: s.scenarios ?? [],
126
- spec: { name: s.name, transport: { kind: "stdio", command: s.transport.command, args: s.transport.args, ...(Object.keys(env).length ? { env } : {}) }, ...allow, ...elicit, ...contentOrigin, ...source, ...toolFaces },
143
+ spec: { name: s.name, transport: { kind: "stdio", command: s.transport.command, args: s.transport.args, ...(Object.keys(env).length ? { env } : {}) }, ...allow, ...elicit, ...contentOrigin, ...source, ...toolFaces, ...toolAxes },
127
144
  });
128
145
  }
129
146
  else {
130
147
  const headers = resolveRefs(s.transport.headerRefs, s.name, "header", logger);
131
148
  if (headers === null)
132
149
  continue;
150
+ if (s.transport.assertionHeader !== undefined)
151
+ noteMcpAssertionHeaderNotSent(logger, s.name);
133
152
  out.push({
134
153
  scenarios: s.scenarios ?? [],
135
- spec: { name: s.name, transport: { kind: "http", url: s.transport.url, ...(Object.keys(headers).length ? { headers } : {}), ...(s.transport.principalHeader ? { principalHeader: s.transport.principalHeader } : {}) }, ...allow, ...elicit, ...contentOrigin, ...source, ...toolFaces },
154
+ spec: { name: s.name, transport: { kind: "http", url: s.transport.url, ...(Object.keys(headers).length ? { headers } : {}), ...(s.transport.principalHeader ? { principalHeader: s.transport.principalHeader } : {}) }, ...allow, ...elicit, ...contentOrigin, ...source, ...toolFaces, ...toolAxes },
136
155
  });
137
156
  }
138
157
  }
@@ -151,6 +151,18 @@ export interface CenterMcpServer {
151
151
  * 形上先写一个结构,等于把同一张判据表抄成两份(而其中一份没有执行力)。
152
152
  */
153
153
  toolFaces?: unknown;
154
+ /**
155
+ * S-1475(settings-schema 12.2.x `McpServerSpec.toolAxes`,解析透明 `z.unknown()`)—— 部署侧为这台 server 的**每只工具**声明的
156
+ * 风险轴(`effect` / `egress` / `irreversibility`),键 = server 的**原始(未加命名空间)**工具名,上 core
157
+ * `McpServerSpec.toolAxes`。
158
+ *
159
+ * 🔴 类型是 `unknown` 而不是那个结构,**刻意的**(与 {@link CenterMcpServer.toolFaces} 逐字同姿):本类型是两条部署腿的**承载形**
160
+ * (本地 config.d 经 `z.unknown()` 解析透明地过来、远端 /effective 是直读 JSON),形判与投影的单一属主是 `mcp-tool-axes.ts` 的
161
+ * `readMcpToolAxes`(`resolveMcpServers` 在铸 spec 那一刻调它;产出的型是引擎的 `McpServerSpec["toolAxes"]`)。坏形 ⇒ 整条
162
+ * server 不进 + 一行 `web_admin_mcp_tool_axes_invalid`。
163
+ * (同批解析透明键 `authForm` / `entryKind` 的语义归连接器,本层仍只承载。)
164
+ */
165
+ toolAxes?: unknown;
154
166
  transport: {
155
167
  kind: "stdio";
156
168
  command: string;
@@ -160,11 +172,17 @@ export interface CenterMcpServer {
160
172
  /** `principalHeader` ([ref]): the header name into which the runner injects the task's authenticated
161
173
  * end-user principal per-task, so a consumer MCP (e.g. oa-mcp) can enforce per-user RBAC. Not a ref — a
162
174
  * literal header name. The runner fills the value (never the model). */
175
+ /** `assertionHeader` (settings-schema ≥12.2.0): the header NAME a hosted connector's assertion travels in. Mutually exclusive with
176
+ * `principalHeader` (both answer "which header carries the identity this server sends"; the contract package refuses both-set at parse
177
+ * time — the request face enforces the same rule at acceptance, see `mcp-assertion-header.ts`). A literal header name, not a ref.
178
+ * This version CARRIES it up to the minting of the engine spec and does not send it (the engine's http transport has no seat yet):
179
+ * `resolveMcpServers` leaves it off the spec and says one info line per mounted server. */
163
180
  | {
164
181
  kind: "http";
165
182
  url: string;
166
183
  headerRefs?: Record<string, string>;
167
184
  principalHeader?: string;
185
+ assertionHeader?: string;
168
186
  };
169
187
  }
170
188
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.127.2",
3
+ "version": "7.128.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",