@sema-agent/server 7.127.1 → 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/MIGRATION.md +39 -0
- package/USAGE.md +53 -3
- package/dist/audit.js +4 -1
- package/dist/boot/execution-lane.d.ts +11 -0
- package/dist/boot/execution-lane.js +6 -0
- package/dist/boot/resolve-spec.js +3 -1
- package/dist/boot/web-admin.js +15 -11
- package/dist/capabilities/mcp-probe.js +11 -5
- package/dist/config-catalog.js +1 -1
- package/dist/config-provider.d.ts +17 -4
- package/dist/config-provider.js +8 -6
- package/dist/config-types.d.ts +2 -2
- package/dist/hooks/hook-runner.d.ts +1 -0
- package/dist/hooks/hook-runner.js +10 -4
- package/dist/http/wire-types.d.ts +11 -3
- package/dist/main.js +2 -0
- package/dist/mcp-assertion-header.d.ts +89 -0
- package/dist/mcp-assertion-header.js +49 -0
- package/dist/mcp-tool-axes.d.ts +62 -0
- package/dist/mcp-tool-axes.js +47 -0
- package/dist/observability/fail-open.d.ts +1 -1
- package/dist/observability/fail-open.js +1 -1
- package/dist/plugins/host-platform.d.ts +43 -13
- package/dist/plugins/host-platform.js +67 -18
- package/dist/plugins/remote-env-host.js +3 -3
- package/dist/run-local.js +5 -3
- package/dist/task-mcp.d.ts +31 -3
- package/dist/task-mcp.js +42 -4
- package/dist/trace/project.d.ts +3 -1
- package/dist/trace/project.js +2 -2
- package/dist/trace/self-minted-wire.d.ts +19 -0
- package/dist/trace/self-minted-wire.js +2 -0
- package/dist/trace/tool-output-media.d.ts +21 -8
- package/dist/trace/tool-output-media.js +44 -17
- package/dist/web-admin-client/facade.d.ts +1 -1
- package/dist/web-admin-client/facade.js +1 -1
- package/dist/web-admin-client/http-client.d.ts +2 -2
- package/dist/web-admin-client/skills-mcp.d.ts +19 -16
- package/dist/web-admin-client/skills-mcp.js +27 -8
- package/dist/web-admin-client/types.d.ts +18 -0
- package/package.json +1 -1
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
|
-
...(
|
|
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
|
};
|
package/dist/trace/project.d.ts
CHANGED
|
@@ -266,7 +266,9 @@ export declare function reasoningEndEventData(ev: Record<string, unknown>): Segm
|
|
|
266
266
|
* core 1.203: `structured` = the tool's CC-shaped `details` card (edit/create/bash/… — core
|
|
267
267
|
* allowlists the card `type` and size-caps the whole card, omit-whole on overflow, so a subagent's child-TaskResult
|
|
268
268
|
* can never appear here) — SAME UNTRUSTED RAW contract as `output` (a card carries `originalFile`/stdout bodies),
|
|
269
|
-
* so it gets the same deep redaction before it is persisted or echoed
|
|
269
|
+
* so it gets the same deep redaction before it is persisted or echoed — except a media card's `file.base64` (image / document
|
|
270
|
+
* cards: the second encoding of the same bytes the `output` block carries), which, like the block's `data`, is an encoding of
|
|
271
|
+
* bytes and not text ({@link projectToolStructured}; the durable face drops it in the ledger confluence). `label`(core 展示名,同 tool_start 的
|
|
270
272
|
* label 待遇 verbatim)白名单排查(2026-07-24)补齐进函数本体——此前外包给调用点各自手动拼接,7 处只有
|
|
271
273
|
* 3 处记得拼,resume leg + ledger-sink 落盘两处漏了,持久化记录本身缺 label 一写盘就永久追不回来。 */
|
|
272
274
|
export declare function toolEndEventData(ev: {
|
package/dist/trace/project.js
CHANGED
|
@@ -3,7 +3,7 @@ import { isAskClass, isAskOrigin, isModeReleaseReason, isPermissionMode, isPersi
|
|
|
3
3
|
import { Value } from "typebox/value";
|
|
4
4
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
5
5
|
import { redactSecrets, redactDeep } from "./redact.js";
|
|
6
|
-
import { projectToolOutput } from "./tool-output-media.js";
|
|
6
|
+
import { projectToolOutput, projectToolStructured } from "./tool-output-media.js";
|
|
7
7
|
import { injectionTierOrAbsent } from "./injection-tier.js";
|
|
8
8
|
import { semaProvenanceOrAbsent } from "./sema-provenance.js";
|
|
9
9
|
import { resupplyFields, terminalFrameFacetFields } from "./task-notification-facets.js";
|
|
@@ -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:
|
|
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 } : {}),
|
|
@@ -509,6 +509,25 @@ export declare const SELF_MINTED_WIRE: {
|
|
|
509
509
|
readonly attr: "durableMediaDescriptor+totalChars";
|
|
510
510
|
readonly since: "7.127.0";
|
|
511
511
|
};
|
|
512
|
+
/**
|
|
513
|
+
* 7.127.2:同一条律的第二个座位 —— `tool_end.structured` 的**媒体卡**(Read 的图像 / 文档 details 卡 `{ type: "image" | "document", file: { base64?, … } }`)的
|
|
514
|
+
* `file.base64` 是同一份字节的第二份编码:活面逐字过境(不跑文本脱敏,卡上其余成员照常深度脱敏)、同一个连接积压判据一并计、降级时一并去掉;
|
|
515
|
+
* 钉 test/tool-end-media-durable.test.ts、test/live-media-backlog.test.ts。7.127.0 / 7.127.1 上这个键还过文本脱敏,且积压判据不计它。
|
|
516
|
+
*/
|
|
517
|
+
readonly "SSE tool_end#structured.file.base64(\u5A92\u4F53\u5361)": {
|
|
518
|
+
readonly attr: "verbatim-bytes;backlog>64MiB⇒key-dropped";
|
|
519
|
+
readonly since: "7.127.2";
|
|
520
|
+
};
|
|
521
|
+
/**
|
|
522
|
+
* 7.127.2:同一媒体卡在**持久面**(账本行与 `GET /v1/runs/:id/events` / `GET /v1/tasks/:id/turns` / trace SSE 回放)只剩卡形自己的「不带字节」形 ——
|
|
523
|
+
* `file` 上没有 `base64` 键,其余成员逐字;帧级 `truncated` / `totalChars` 不因剥卡而动(它们只说 `output`)(账本汇点同一次调用剥:
|
|
524
|
+
* `redactLedgerEventData` → `toolEndMediaAsDescriptors`;钉 test/tool-end-media-durable.test.ts、test/wire-structured-events.test.ts)。
|
|
525
|
+
* 7.127.0 / 7.127.1 写下的存量行在读面上同样不再带字节。
|
|
526
|
+
*/
|
|
527
|
+
readonly "GET /v1/runs/:id/events#tool_end.structured.file.base64(\u5A92\u4F53\u5361)": {
|
|
528
|
+
readonly attr: "durableMediaCardKeyDropped";
|
|
529
|
+
readonly since: "7.127.2";
|
|
530
|
+
};
|
|
512
531
|
};
|
|
513
532
|
};
|
|
514
533
|
};
|
|
@@ -171,6 +171,8 @@ export const SELF_MINTED_WIRE = {
|
|
|
171
171
|
"engine_notice permission.rule_row_dropped#detail.rows[].rule": { attr: "redactListEntry", since: "7.120.0" },
|
|
172
172
|
"SSE tool_end#output[].data(媒体块)": { attr: "verbatim-bytes;backlog>64MiB⇒descriptor+totalChars", since: "7.127.0" },
|
|
173
173
|
"GET /v1/runs/:id/events#tool_end.output[](媒体块)": { attr: "durableMediaDescriptor+totalChars", since: "7.127.0" },
|
|
174
|
+
"SSE tool_end#structured.file.base64(媒体卡)": { attr: "verbatim-bytes;backlog>64MiB⇒key-dropped", since: "7.127.2" },
|
|
175
|
+
"GET /v1/runs/:id/events#tool_end.structured.file.base64(媒体卡)": { attr: "durableMediaCardKeyDropped", since: "7.127.2" },
|
|
174
176
|
},
|
|
175
177
|
},
|
|
176
178
|
};
|
|
@@ -4,21 +4,34 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export declare function projectToolOutput(output: unknown): unknown;
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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` 必须是整值→整值的纯函数(环 / 深度守卫在它里面,遍历不重做);非媒体卡原样交给它,所以非媒体卡的读数就是调用面自己的深扫。
|
|
14
|
+
*/
|
|
15
|
+
export declare function projectToolStructured(structured: unknown, redact: (value: unknown) => unknown): unknown;
|
|
16
|
+
/**
|
|
17
|
+
* `tool_end` 载荷的媒体降级(持久面恒用;活面只在连接积压时用),**一次调用降两个座位**:
|
|
18
|
+
* · `output`:带 `data` 的媒体块换成描述符,有任何一块被换 ⇒ 帧级 `truncated: true` 且帧级 `totalChars` 在场 —— core 不变量
|
|
19
|
+
* 「Present iff truncated」:被界之前那个值的真实大小。帧上已有 `totalChars`(core 已截断:文本被切而媒体留着)⇒ **原样保留**
|
|
20
|
+
* (它说的是更早的原值,本仓手里的已是切过的值,重算只会低报);没有 ⇒ 按 {@link trueSizeOf} 逐块求和(core 没截,`output` 就是原值 ——
|
|
21
|
+
* 文本块是本服务脱敏后的字节,即本服务交付的内容)。已是描述符的块(无 `data`)原样 ⇒ 幂等。
|
|
22
|
+
* · `structured`:媒体卡去掉 `file.base64`(core 卡形自己的「不带字节」形),其余成员逐字。帧级 `truncated` / `totalChars` 只说 `output`,
|
|
23
|
+
* 单独剥卡**不动**它们(`output` 已是 core 描述符而卡仍带 ≤48K 字节的帧,和卡超 48K 时 core 自己略去 `base64` 的帧,在这两个键上读数一致)。
|
|
24
|
+
* 两处都没有媒体字节 ⇒ 原样返回同一引用(纯文本 / 串形 output / 非媒体卡一个字节不变)。
|
|
12
25
|
*/
|
|
13
26
|
export declare function toolEndMediaAsDescriptors(data: Record<string, unknown>): Record<string, unknown>;
|
|
14
27
|
/** 活面一条连接允许压住的待发字节上界(含本帧媒体)。一帧媒体最大约为部署媒体预算的 4/3(缺省约 40 MiB),
|
|
15
28
|
* 上界取它之上一点:空闲连接上一整帧最大的图照样整块过,第二帧起只在读者跟上之后才整块。 */
|
|
16
29
|
export declare const LIVE_MEDIA_BACKLOG_BYTES: number;
|
|
17
|
-
/** 一帧 `tool_end`
|
|
30
|
+
/** 一帧 `tool_end` 载荷里媒体字节的串长(base64):`output` 媒体块的 `data` 与 `structured` 媒体卡的 `file.base64` 一起计(同一帧、同一条连接上的字节;没有媒体 = 0)。 */
|
|
18
31
|
export declare function liveMediaBytesOf(frame: Record<string, unknown>): number;
|
|
19
32
|
/**
|
|
20
|
-
* 活面的积压判据(一只):`backlogBytes` = 这条连接 / 这个订阅者此刻还没送出去的字节。本帧媒体 + 积压 > 上界
|
|
21
|
-
*
|
|
33
|
+
* 活面的积压判据(一只):`backlogBytes` = 这条连接 / 这个订阅者此刻还没送出去的字节。本帧媒体(`output` 块 + `structured` 卡的字节一起计)+ 积压 > 上界
|
|
34
|
+
* ⇒ 媒体降持久形(块换描述符、卡去 `file.base64`,同一只 {@link toolEndMediaAsDescriptors}),否则原样(同一引用)。非 `tool_end` 载荷由调用方不送进来;没有媒体的 `tool_end` 恒原样。
|
|
22
35
|
* 降级是 F 类兜底(观众少见一张图),每降一帧在**这里**记一次 `server.tool-end-media.live-backlog-degraded` —— 三处写口都经这一只函数,
|
|
23
36
|
* 留痕因此只有一处;帧上它与引擎的媒体预算降级同形,计数是运维看出「多少观众丢了图」的唯一位。
|
|
24
37
|
*/
|
|
@@ -12,6 +12,21 @@ export function projectToolOutput(output) {
|
|
|
12
12
|
return redactDeep(output);
|
|
13
13
|
return output.map((b) => (isMediaBlock(b) ? Object.fromEntries(Object.entries(b).map(([k, v]) => [k, k === "data" ? v : redactDeep(v)])) : redactDeep(b)));
|
|
14
14
|
}
|
|
15
|
+
function isMediaCard(s) {
|
|
16
|
+
if (s === null || typeof s !== "object" || Array.isArray(s))
|
|
17
|
+
return false;
|
|
18
|
+
const card = s;
|
|
19
|
+
if (card.type !== "image" && card.type !== "document")
|
|
20
|
+
return false;
|
|
21
|
+
const file = card.file;
|
|
22
|
+
return file !== null && typeof file === "object" && !Array.isArray(file) && typeof file.base64 === "string";
|
|
23
|
+
}
|
|
24
|
+
export function projectToolStructured(structured, redact) {
|
|
25
|
+
if (!isMediaCard(structured))
|
|
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
|
+
}
|
|
15
30
|
function trueSizeOf(b) {
|
|
16
31
|
if (isMediaBlock(b))
|
|
17
32
|
return b.data.length;
|
|
@@ -23,28 +38,40 @@ function trueSizeOf(b) {
|
|
|
23
38
|
}
|
|
24
39
|
export function toolEndMediaAsDescriptors(data) {
|
|
25
40
|
const output = data.output;
|
|
26
|
-
|
|
41
|
+
const outputHasMedia = Array.isArray(output) && output.some(isMediaBlock);
|
|
42
|
+
const structured = data.structured;
|
|
43
|
+
if (!outputHasMedia && !isMediaCard(structured))
|
|
27
44
|
return data;
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
const next = { ...data };
|
|
46
|
+
if (outputHasMedia) {
|
|
47
|
+
const blocks = output;
|
|
48
|
+
const totalChars = typeof data.totalChars === "number" && Number.isFinite(data.totalChars) ? data.totalChars : blocks.reduce((n, b) => n + trueSizeOf(b), 0);
|
|
49
|
+
next.output = blocks.map((b) => isMediaBlock(b)
|
|
50
|
+
? {
|
|
51
|
+
type: b.type,
|
|
52
|
+
...(typeof b.mimeType === "string" && b.mimeType.length <= MIME_ECHO_MAX ? { mimeType: b.mimeType } : {}),
|
|
53
|
+
bytes: Math.floor((b.data.length * 3) / 4),
|
|
54
|
+
truncated: true,
|
|
55
|
+
}
|
|
56
|
+
: b);
|
|
57
|
+
next.truncated = true;
|
|
58
|
+
next.totalChars = totalChars;
|
|
59
|
+
}
|
|
60
|
+
if (isMediaCard(structured)) {
|
|
61
|
+
const { base64: _bytes, ...file } = structured.file;
|
|
62
|
+
next.structured = { ...structured, file };
|
|
63
|
+
}
|
|
64
|
+
return next;
|
|
38
65
|
}
|
|
39
66
|
export const LIVE_MEDIA_BACKLOG_BYTES = 64 * 1024 * 1024;
|
|
40
67
|
export function liveMediaBytesOf(frame) {
|
|
68
|
+
const structured = frame.structured;
|
|
69
|
+
let n = isMediaCard(structured) ? structured.file.base64.length : 0;
|
|
41
70
|
const output = frame.output;
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (isMediaBlock(b))
|
|
47
|
-
n += b.data.length;
|
|
71
|
+
if (Array.isArray(output))
|
|
72
|
+
for (const b of output)
|
|
73
|
+
if (isMediaBlock(b))
|
|
74
|
+
n += b.data.length;
|
|
48
75
|
return n;
|
|
49
76
|
}
|
|
50
77
|
export function liveToolEndForBacklog(frame, backlogBytes) {
|
|
@@ -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 {
|
|
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 {
|
|
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
|
-
*
|
|
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 {
|
|
5
|
+
import type { ConfigProvider } from "../config-provider.js";
|
|
6
6
|
import type { CenterA2aPeer, CenterMcpServer, CenterSkillManifest } from "./types.js";
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
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.
|
|
28
|
-
* `
|
|
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 {
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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: "
|
|
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.
|
|
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",
|