@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.
Files changed (41) hide show
  1. package/MIGRATION.md +39 -0
  2. package/USAGE.md +53 -3
  3. package/dist/audit.js +4 -1
  4. package/dist/boot/execution-lane.d.ts +11 -0
  5. package/dist/boot/execution-lane.js +6 -0
  6. package/dist/boot/resolve-spec.js +3 -1
  7. package/dist/boot/web-admin.js +15 -11
  8. package/dist/capabilities/mcp-probe.js +11 -5
  9. package/dist/config-catalog.js +1 -1
  10. package/dist/config-provider.d.ts +17 -4
  11. package/dist/config-provider.js +8 -6
  12. package/dist/config-types.d.ts +2 -2
  13. package/dist/hooks/hook-runner.d.ts +1 -0
  14. package/dist/hooks/hook-runner.js +10 -4
  15. package/dist/http/wire-types.d.ts +11 -3
  16. package/dist/main.js +2 -0
  17. package/dist/mcp-assertion-header.d.ts +89 -0
  18. package/dist/mcp-assertion-header.js +49 -0
  19. package/dist/mcp-tool-axes.d.ts +62 -0
  20. package/dist/mcp-tool-axes.js +47 -0
  21. package/dist/observability/fail-open.d.ts +1 -1
  22. package/dist/observability/fail-open.js +1 -1
  23. package/dist/plugins/host-platform.d.ts +43 -13
  24. package/dist/plugins/host-platform.js +67 -18
  25. package/dist/plugins/remote-env-host.js +3 -3
  26. package/dist/run-local.js +5 -3
  27. package/dist/task-mcp.d.ts +31 -3
  28. package/dist/task-mcp.js +42 -4
  29. package/dist/trace/project.d.ts +3 -1
  30. package/dist/trace/project.js +2 -2
  31. package/dist/trace/self-minted-wire.d.ts +19 -0
  32. package/dist/trace/self-minted-wire.js +2 -0
  33. package/dist/trace/tool-output-media.d.ts +21 -8
  34. package/dist/trace/tool-output-media.js +44 -17
  35. package/dist/web-admin-client/facade.d.ts +1 -1
  36. package/dist/web-admin-client/facade.js +1 -1
  37. package/dist/web-admin-client/http-client.d.ts +2 -2
  38. package/dist/web-admin-client/skills-mcp.d.ts +19 -16
  39. package/dist/web-admin-client/skills-mcp.js +27 -8
  40. package/dist/web-admin-client/types.d.ts +18 -0
  41. package/package.json +1 -1
@@ -0,0 +1,89 @@
1
+ /**
2
+ * http 传输的 `assertionHeader` —— 托管连接器「断言头」的**头名**(settings-schema ≥12.2.0 的 `McpTransport` http 形键)。
3
+ *
4
+ * ## 一条律:一个键一种读法
5
+ * 两个面收下这个键,读法是同一个:
6
+ * · **settings 面**(部署声明:中心下发 + 本地 `config.d`)—— 契约包在**解析期**受理它,并判它与 `principalHeader` 互斥
7
+ * (同设 = 两个答案:「这台 http 服务器的请求上用哪个头送身份」);本仓这一面只承载,铸 spec 在 `resolveMcpServers`;
8
+ * · **请求面**(任务提交体 `mcpServers[].transport`;探测口 `…/mcp/probe` 读同一张键表)—— 本仓在**受理期**兑现同一条规则:
9
+ * 受理 = 字符串才收;互斥 = 同设即拒;投影 = 铸 spec 时不投(见下)。
10
+ * 同一条规则、两处执行者(契约包解析期 / 请求面受理期);判据 {@link mcpIdentityHeadersIssue} 与互斥拒句
11
+ * {@link describeMcpIdentityHeadersConflict} 对契约包的原判官**逐字**对齐 —— 不是靠这句注释,而是
12
+ * `test/task-mcp.test.ts` 的 S-1515 组把两边对拍(格子差分 + 拒句逐字相等),契约包改一个字那一组当场红。
13
+ *
14
+ * ## 后果按腿分(判据同一只,后果不同 —— 本仓成文的 `opts.leg` 姿势)
15
+ * · **fresh 提交腿**:判出问题 ⇒ 同步 400(同设 `request.body_shape`;值不是字符串 `request.field_invalid`)。
16
+ * · **其余腿(耐久 resume 重放存量 body)**:**不拒、不丢 server** —— 存量 body 里这个键当时是被静默忽略的,server 当时是挂着的;
17
+ * 此刻把 server 摘掉,一条 park 在该 server 工具审批上的任务批准后会撞 `resume.tool_unavailable` 并重开检查点,而每次重试重放同一份 body ⇒ 永远卡住。
18
+ * 所以读器 {@link readMcpIdentityHeaders} 的处置是**降级到升级前的读法**:`principalHeader` 胜(照挂,与升级前逐字相同),`assertionHeader` 不生效;
19
+ * 不静默 —— 真挂上的这类条目由 {@link noteMcpAssertionHeaderIgnored} 说一行 warn(部署声明面没有这一格:配置不是被重放的存量任务,契约包解析期那一刀直接丢这一条)。
20
+ *
21
+ * ## 发送归引擎座(本版只承载,不发送)
22
+ * core 的 http 传输只有 `url` / `headers` / `principalHeader`,**没有**断言头座。所以本键承载到「铸 core 规格」为止:
23
+ * 请求面在 {@link toCoreMcpServerSpec} 剥掉它,settings 面的 `resolveMcpServers` 不投它;两处都用同一只
24
+ * {@link noteMcpAssertionHeaderNotSent} 说出一行 info(声明在、发送未接)—— 披露,不是兜底:server 照常挂载、不带断言头,
25
+ * 要求断言的托管连接器会拒这些调用,运维据这一行知道原因在哪。
26
+ *
27
+ * ## 退役(编译期自动逼,零人工记忆)
28
+ * 引擎的 http 传输一旦长出同名座,{@link _CoreHasNoAssertionSeat} 当场编译红:届时删本文件里的 {@link RequestMcpServerSpec}
29
+ * 扩展与 {@link toCoreMcpServerSpec} 的剥键、两处 info,把键直接投进引擎规格(发送由引擎座兑现)。
30
+ */
31
+ import type { McpServerSpec } from "@sema-agent/core";
32
+ type CoreMcpTransport = McpServerSpec["transport"];
33
+ type CoreStdioTransport = Extract<CoreMcpTransport, {
34
+ kind: "stdio";
35
+ }>;
36
+ type CoreHttpTransport = Extract<CoreMcpTransport, {
37
+ kind: "http";
38
+ }>;
39
+ /** 请求面 http 传输的形:引擎的 http 传输 + 本仓承载的 `assertionHeader`(引擎无座,见头注)。 */
40
+ export type RequestHttpMcpTransport = CoreHttpTransport & {
41
+ assertionHeader?: string;
42
+ };
43
+ /**
44
+ * 请求面一条 `mcpServers[]` 条目在 wire 上的形(= `TaskRequestBody.mcpServers` 的元素型,也是 `normalizeServer` 的产物):
45
+ * 引擎的 {@link McpServerSpec} 加上 http 传输上的 `assertionHeader`。交给引擎之前一律经 {@link toCoreMcpServerSpec}。
46
+ */
47
+ export type RequestMcpServerSpec = Omit<McpServerSpec, "transport"> & {
48
+ transport: CoreStdioTransport | RequestHttpMcpTransport;
49
+ };
50
+ /** 一份原始 http 传输上两个身份头的问题类:`both_set` = 同设了 `principalHeader` 与 `assertionHeader`;`not_a_string` = `assertionHeader` 在场但不是字符串。 */
51
+ export type McpIdentityHeadersIssue = "both_set" | "not_a_string";
52
+ /**
53
+ * 请求面对一份**原始**传输对象(判的是调用方写了什么,不是读器留下什么)的唯一判官,fresh 门 / 探测口 / 重放腿的披露三处共读它。
54
+ * · `both_set` 与契约包 `McpServerSpec` 的 `superRefine` 同一判据(http 形 ∧ 两键都已设,`!== undefined`);
55
+ * · `not_a_string`:`assertionHeader` 在场而非字符串 —— 契约包的 zod 类型拒它,请求面不能把它悄悄丢成"没写过"。
56
+ * 两类同时成立时报 `both_set`(先名字后值)。stdio 传输不判(两键在 stdio 形上本就不存在,契约包同判)。
57
+ */
58
+ export declare function mcpIdentityHeadersIssue(transport: Record<string, unknown>): McpIdentityHeadersIssue | undefined;
59
+ /**
60
+ * 读器的这一格(`normalizeServer` 的 http 传输读两个身份头):字符串才收;**`principalHeader` 胜**——它在场(不论值形)时 `assertionHeader` 不收。
61
+ * 于是 normalize 后的形**结构上不可能同带两键**(`mcpIdentityHeadersIssue` 的 `both_set` 在读器之后不存在),重放存量 body 的处置因此就是升级前的读法。
62
+ */
63
+ export declare function readMcpIdentityHeaders(transport: Record<string, unknown>): {
64
+ principalHeader?: string;
65
+ assertionHeader?: string;
66
+ };
67
+ /** 同设的拒句 —— 对契约包解析期那一句**逐字**(`server` = 条目名,调用方负责清洗后传入)。 */
68
+ export declare function describeMcpIdentityHeadersConflict(server: string): string;
69
+ /** `assertionHeader` 值不是字符串的拒句(值的形状只说种类,不回显值本身:它是一个调用方写的头名位置,可能是任何东西)。 */
70
+ export declare function describeMcpAssertionHeaderNotAString(server: string, value: unknown): string;
71
+ /** 这一条请求面条目是否声明了 `assertionHeader`(= 铸 core 规格时会被剥掉、需要说一行的那一类)。 */
72
+ export declare function declaresMcpAssertionHeader(spec: RequestMcpServerSpec): boolean;
73
+ /** 请求面条目 → 交给引擎的规格:剥掉 `assertionHeader`(引擎无座);其余逐字不动。声明过它的条目由调用方另说一行 info。 */
74
+ export declare function toCoreMcpServerSpec(spec: RequestMcpServerSpec): McpServerSpec;
75
+ /** `noteMcpAssertionHeaderNotSent` 的日志行名(运维 grep 用;请求面与 settings 面同一个名字)。 */
76
+ export declare const MCP_ASSERTION_HEADER_NOT_SENT = "mcp_assertion_header_not_sent";
77
+ /** `noteMcpAssertionHeaderIgnored` 的日志行名。 */
78
+ export declare const MCP_ASSERTION_HEADER_IGNORED = "mcp_assertion_header_ignored";
79
+ /** 重放腿(存量 body)上一条**真挂上**的 server 带着有问题的 `assertionHeader` 声明:读器按升级前的读法处置了它(见头注「后果按腿分」),这里说出来 —— 一台一行 warn。 */
80
+ export declare function noteMcpAssertionHeaderIgnored(logger: {
81
+ warn?(msg: string, meta?: unknown): void;
82
+ } | undefined, server: string, reason: McpIdentityHeadersIssue): void;
83
+ /** 说出「这台 server 声明了 `assertionHeader`,本版没有发送」—— 一台一行 info。两个铸 spec 点(请求面 `classifyHonored`、
84
+ * settings 面 `resolveMcpServers`)共用这一只,所以行名与措辞只有一份。 */
85
+ export declare function noteMcpAssertionHeaderNotSent(logger: {
86
+ info?(msg: string, meta?: unknown): void;
87
+ } | undefined, server: string): void;
88
+ export {};
89
+ //# sourceMappingURL=mcp-assertion-header.d.ts.map
@@ -0,0 +1,49 @@
1
+ export function mcpIdentityHeadersIssue(transport) {
2
+ if (transport.kind !== "http")
3
+ return undefined;
4
+ if (transport.principalHeader !== undefined && transport.assertionHeader !== undefined)
5
+ return "both_set";
6
+ if (transport.assertionHeader !== undefined && typeof transport.assertionHeader !== "string")
7
+ return "not_a_string";
8
+ return undefined;
9
+ }
10
+ export function readMcpIdentityHeaders(transport) {
11
+ return {
12
+ ...(typeof transport.principalHeader === "string" ? { principalHeader: transport.principalHeader } : {}),
13
+ ...(transport.principalHeader === undefined && typeof transport.assertionHeader === "string" ? { assertionHeader: transport.assertionHeader } : {}),
14
+ };
15
+ }
16
+ export function describeMcpIdentityHeadersConflict(server) {
17
+ return `mcp server "${server}" sets both principalHeader and assertionHeader — an http entry names ONE header for the identity it sends: keep principalHeader (the end-user principal) or assertionHeader (the connector assertion), not both`;
18
+ }
19
+ export function describeMcpAssertionHeaderNotAString(server, value) {
20
+ const kind = value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
21
+ return `mcp server "${server}" sets assertionHeader to a ${kind} — it must be a string (the HTTP header NAME the connector assertion travels in); omit the key to send no assertion header`;
22
+ }
23
+ export function declaresMcpAssertionHeader(spec) {
24
+ return spec.transport.kind === "http" && spec.transport.assertionHeader !== undefined;
25
+ }
26
+ export function toCoreMcpServerSpec(spec) {
27
+ if (spec.transport.kind !== "http" || spec.transport.assertionHeader === undefined)
28
+ return spec;
29
+ const { assertionHeader: _declaredNotSent, ...transport } = spec.transport;
30
+ return { ...spec, transport };
31
+ }
32
+ export const MCP_ASSERTION_HEADER_NOT_SENT = "mcp_assertion_header_not_sent";
33
+ export const MCP_ASSERTION_HEADER_IGNORED = "mcp_assertion_header_ignored";
34
+ export function noteMcpAssertionHeaderIgnored(logger, server, reason) {
35
+ logger?.warn?.(MCP_ASSERTION_HEADER_IGNORED, {
36
+ server,
37
+ reason,
38
+ note: reason === "both_set"
39
+ ? "assertionHeader declaration IGNORED — principalHeader is also set and wins (the server mounts exactly as it did before assertionHeader was accepted); a new submission with both keys is refused with 400"
40
+ : "assertionHeader declaration IGNORED — the value is not a string (the server mounts without it, exactly as before assertionHeader was accepted); a new submission with such a value is refused with 400",
41
+ });
42
+ }
43
+ export function noteMcpAssertionHeaderNotSent(logger, server) {
44
+ logger?.info?.(MCP_ASSERTION_HEADER_NOT_SENT, {
45
+ server,
46
+ note: "declaration KEPT but NOT SENT — the engine's http transport has no seat for an assertion header yet, so this server mounts WITHOUT one (a hosted connector that requires the assertion will refuse its calls); sending follows the engine seat",
47
+ });
48
+ }
49
+ //# sourceMappingURL=mcp-assertion-header.js.map
@@ -0,0 +1,62 @@
1
+ /**
2
+ * `McpServerSpec.toolAxes` —— 部署侧为一台 MCP 服务器的**每只工具**声明的风险轴(`effect` / `egress` /
3
+ * `irreversibility`),键 = 该 server 的**原始(未加命名空间)**工具名。声明面 = settings-schema 12.2.x 的 MCP 条目
4
+ * 解析透明键(`z.unknown()`);引擎侧 = `McpServerSpec.toolAxes`(喂审批门与计划模式围栏的信任根轴)。
5
+ *
6
+ * ## 一只读器、一处投影
7
+ * 两条部署腿(本地 `config.d/mcp.json` 经 `LocalConfigProvider` / 中心 `/effective.mcp` 经远端读腿)都落到
8
+ * `web-admin-client/skills-mcp.ts resolveMcpServers` 这一只铸 spec 的函数(boot 与 run-local 共用),所以本模块
9
+ * 的形判与投影**只有这一处**;本地承载腿(`config-provider.ts`)只搬运、不判形 —— 与 `mcp-tool-faces.ts`
10
+ * 同一种「判据单点」的形。
11
+ *
12
+ * ## 词表来自哪(本模块一个词都不枚举)
13
+ * `effect` / `irreversibility` 两个闭集词轴的判据是 `tool-axis-words.ts` 的**共用谓词**(`isToolEffect` /
14
+ * `isToolAxisIrreversibility`;那一份词表的两向门钉死在引擎的型上:引擎加词 / 删词 ⇒ 编译红)。`egress` 是裸布尔,
15
+ * 没有词表。条目的**成员集**同样不抄一份:`AXIS_MEMBERS` 以 `satisfies Record<keyof ToolAxisEntry, true>` 取引擎型的键,
16
+ * 引擎给轴条目加新成员 ⇒ 这里编译红(而不是把一个合法的新成员当未知键拒、或当已知键漏判)。
17
+ *
18
+ * ## 为什么本层只判形,不判语义组合
19
+ * 轴值之间的语义关系(例如 `effect:"read"` 与 `egress:true` 并存)归引擎的摄入侧判 —— 与 `toolFaces` 同一条分工:
20
+ * 本层不复述引擎的规则表,只做引擎的校验器**假定成立**的那一件事:JSON 结构合不合 TS 型。原样透传一份任意 JSON
21
+ * 不是「解析透明」,是把类型错误变成运行期的更难读的失败。
22
+ *
23
+ * ## 坏形的方向:整条 server 不进(fail-closed;与 `toolFaces` 同方向)
24
+ * `toolAxes` 是**双向**声明:`effect:"read"` / `egress:false` / `irreversibility:"never"` 能让一次调用免审或降档(放宽),
25
+ * `egress:true` / `irreversibility:"always"` 又能把一次调用的风险抬高(收窄)。所以对任何一个坏形,「丢掉那个键、留下
26
+ * server」都落在安全轴上的静默一侧 —— 要么让一条写错的信任根声明静默失效(运维以为写了保护,引擎读到缺席),要么让它
27
+ * 静默地以残缺形生效(留 `effect:"read"` 丢坏形 `egress`,拼出引擎判为更宽的形)。仓规([ref])不许安全轴上有静默兜底,
28
+ * 判据因此回到本仓对收窄键成文的那一条:**一个键在场就必须合法,否则这条 server 整条不进**。每只工具的条目**整只**判,
29
+ * 而**任一只**判不出形 ⇒ 整条 server 不进(「留下好的那几只」同样是让坏的那一只带着缺失的声明挂载)。
30
+ * ① 谁需要 —— 在部署侧为 MCP / 连接器工具声明效应轴的运维;
31
+ * ② 谁受伤 —— 写错的那台 server:它**整条**不挂(兄弟 server 照挂,名字仍被部署占着、请求腿补不了位)。代价是真的
32
+ * (少一套工具),但它**响亮且立刻**;反方向的代价是一条静默失效或静默走样的信任根声明;
33
+ * ③ 拿什么补偿 —— `web_admin_mcp_tool_axes_invalid{server,problem,note}` 一条点名 warn,`problem` 指到工具名与哪一段不合
34
+ * (只带工具名与成员名,不带值);运维照着改那一行重发布即可。
35
+ *
36
+ * ## 与请求腿的边界(刻意不同的信任根)
37
+ * 请求腿(`task-mcp.ts`)对调用方自带的 `toolAxes` 另有一份**调用方信任根**的读法(单用户闸:调用方对自己的选择负责,
38
+ * 坏成员逐键诚实剔除而不 400)。部署腿声明的是**运维对每一个租户、每一次任务**的担保,没有「调用方自己承担」这回事,
39
+ * 所以坏形必须响亮地拒。两个读器今天并存、方向不同,是两个信任根各自的读法;本模块不替请求腿改判。
40
+ */
41
+ import type { McpServerSpec } from "@sema-agent/core";
42
+ type ToolAxes = NonNullable<McpServerSpec["toolAxes"]>;
43
+ /**
44
+ * 一条 server 条目的 `toolAxes` 判形结果:
45
+ * · `{ ok: true, toolAxes }` —— 合形;`toolAxes` 缺席 = 这台 server 没有声明,或声明的全是空(`{}` / `{ t: {} }`:对引擎同义于缺席),不铸键;
46
+ * · `{ ok: false, problem }` —— 判不出形,**整条 server 不进**(方向与理由见模块头注),`problem` 是给运维的一句点名
47
+ * (只带工具名与成员名,不带值)。
48
+ */
49
+ export type McpToolAxesRead = {
50
+ ok: true;
51
+ toolAxes?: ToolAxes;
52
+ } | {
53
+ ok: false;
54
+ problem: string;
55
+ };
56
+ /**
57
+ * 从一条部署侧 server 条目的原始 `toolAxes` 读出引擎 spec 的 `toolAxes` —— **两条部署腿共用的唯一判据与投影点**。
58
+ * 缺席 ⇒ 合形无声明;整只不是对象 / 工具名为空 / **任一只**工具的条目判不出形 ⇒ `ok:false`(调用方整条 server 不收)。
59
+ */
60
+ export declare function readMcpToolAxes(raw: unknown): McpToolAxesRead;
61
+ export {};
62
+ //# sourceMappingURL=mcp-tool-axes.d.ts.map
@@ -0,0 +1,47 @@
1
+ import { isToolAxisIrreversibility, isToolEffect } from "./tool-axis-words.js";
2
+ const AXIS_MEMBERS = { effect: true, egress: true, irreversibility: true };
3
+ const AXIS_MEMBER_NAMES = Object.keys(AXIS_MEMBERS);
4
+ function isRecord(v) {
5
+ return typeof v === "object" && v !== null && !Array.isArray(v);
6
+ }
7
+ function describeKind(v) {
8
+ return v === null ? "null" : Array.isArray(v) ? "an array" : typeof v;
9
+ }
10
+ function readOneEntry(raw) {
11
+ if (!isRecord(raw))
12
+ return `the declaration is ${describeKind(raw)}, not an object of axes`;
13
+ for (const member of Object.keys(raw)) {
14
+ if (!AXIS_MEMBER_NAMES.includes(member))
15
+ return `unknown member ${JSON.stringify(member)}`;
16
+ }
17
+ const { effect, egress, irreversibility } = raw;
18
+ if (effect !== undefined && !isToolEffect(effect))
19
+ return "member effect is not a word the engine knows";
20
+ if (egress !== undefined && typeof egress !== "boolean")
21
+ return "member egress is not a boolean";
22
+ if (irreversibility !== undefined && !isToolAxisIrreversibility(irreversibility))
23
+ return "member irreversibility is not a word the override face accepts";
24
+ return {
25
+ ...(effect !== undefined ? { effect } : {}),
26
+ ...(egress !== undefined ? { egress } : {}),
27
+ ...(irreversibility !== undefined ? { irreversibility } : {}),
28
+ };
29
+ }
30
+ export function readMcpToolAxes(raw) {
31
+ if (raw === undefined)
32
+ return { ok: true };
33
+ if (!isRecord(raw))
34
+ return { ok: false, problem: `toolAxes is ${describeKind(raw)}, not an object of per-tool axis declarations` };
35
+ const entries = [];
36
+ for (const [tool, rawEntry] of Object.entries(raw)) {
37
+ if (tool.length === 0)
38
+ return { ok: false, problem: "toolAxes has an empty tool name" };
39
+ const entry = readOneEntry(rawEntry);
40
+ if (typeof entry === "string")
41
+ return { ok: false, problem: `the declared axes for tool ${JSON.stringify(tool)} do not fit: ${entry}` };
42
+ if (Object.keys(entry).length > 0)
43
+ entries.push([tool, entry]);
44
+ }
45
+ return entries.length > 0 ? { ok: true, toolAxes: Object.fromEntries(entries) } : { ok: true };
46
+ }
47
+ //# sourceMappingURL=mcp-tool-axes.js.map
@@ -41,7 +41,7 @@ export declare const FAIL_OPEN_TAGS: {
41
41
  };
42
42
  readonly "server.tool-end-media.live-backlog-degraded": {
43
43
  readonly cls: "F";
44
- readonly note: "core 7.50.0 媒体块提货(`trace/tool-output-media.ts` 的 `liveToolEndForBacklog`,三处活面写口共用的那一只判据 —— 同步流出口 `sseData`、子代 tail 路由写环、tail 总线每订阅者队列):本帧媒体字节 + 这条连接 / 这个订阅者还没送出去的字节 > 上界(64 MiB)⇒ 本帧媒体块降成持久面那只描述符(`{ type, mimeType?, bytes, truncated: true }`,帧级 `truncated` + `totalChars`)。每降一帧记一次,只在这只函数里记(三处写口不各记)。放行的最坏后果 = 一个读得慢的观众在 SSE 上看不到这张图,只见「这里有一张多大的什么图、没送达」(账本面本就恒是描述符),不是门、不是凭据、不改任何裁决;反方向(不降)让一条停读的连接随帧数线性压住几百 MiB。帧上它与引擎自己的媒体预算降级同形不可分,所以计数是运维唯一能看出「多少观众丢了图」的位;计数持续非零 = 有消费端读流太慢(或网络瓶颈),去看那几条连接。detail = `backlog=<待发字节> media=<本帧媒体字节>`。";
44
+ readonly note: "core 7.50.0 媒体块提货(`trace/tool-output-media.ts` 的 `liveToolEndForBacklog`,三处活面写口共用的那一只判据 —— 同步流出口 `sseData`、子代 tail 路由写环、tail 总线每订阅者队列):本帧媒体字节(`output` 媒体块的 `data` 与 `structured` 媒体卡的 `file.base64` 一起计)+ 这条连接 / 这个订阅者还没送出去的字节 > 上界(64 MiB)⇒ 本帧媒体降成持久面那只形(媒体块换描述符 `{ type, mimeType?, bytes, truncated: true }`,帧级 `truncated` + `totalChars`;媒体卡去掉 `file.base64`)。每降一帧记一次,只在这只函数里记(三处写口不各记)。放行的最坏后果 = 一个读得慢的观众在 SSE 上看不到这张图,只见「这里有一张多大的什么图、没送达」(账本面本就恒是描述符),不是门、不是凭据、不改任何裁决;反方向(不降)让一条停读的连接随帧数线性压住几百 MiB。帧上它与引擎自己的媒体预算降级同形不可分,所以计数是运维唯一能看出「多少观众丢了图」的位;计数持续非零 = 有消费端读流太慢(或网络瓶颈),去看那几条连接。detail = `backlog=<待发字节> media=<本帧媒体字节>`。";
45
45
  };
46
46
  readonly "server.memory-consolidation.operator-valve-unjudged": {
47
47
  readonly cls: "P-DEBT";
@@ -22,7 +22,7 @@ export const FAIL_OPEN_TAGS = {
22
22
  },
23
23
  "server.tool-end-media.live-backlog-degraded": {
24
24
  cls: "F",
25
- note: "core 7.50.0 媒体块提货(`trace/tool-output-media.ts` 的 `liveToolEndForBacklog`,三处活面写口共用的那一只判据 —— 同步流出口 `sseData`、子代 tail 路由写环、tail 总线每订阅者队列):本帧媒体字节 + 这条连接 / 这个订阅者还没送出去的字节 > 上界(64 MiB)⇒ 本帧媒体块降成持久面那只描述符(`{ type, mimeType?, bytes, truncated: true }`,帧级 `truncated` + `totalChars`)。每降一帧记一次,只在这只函数里记(三处写口不各记)。放行的最坏后果 = 一个读得慢的观众在 SSE 上看不到这张图,只见「这里有一张多大的什么图、没送达」(账本面本就恒是描述符),不是门、不是凭据、不改任何裁决;反方向(不降)让一条停读的连接随帧数线性压住几百 MiB。帧上它与引擎自己的媒体预算降级同形不可分,所以计数是运维唯一能看出「多少观众丢了图」的位;计数持续非零 = 有消费端读流太慢(或网络瓶颈),去看那几条连接。detail = `backlog=<待发字节> media=<本帧媒体字节>`。",
25
+ note: "core 7.50.0 媒体块提货(`trace/tool-output-media.ts` 的 `liveToolEndForBacklog`,三处活面写口共用的那一只判据 —— 同步流出口 `sseData`、子代 tail 路由写环、tail 总线每订阅者队列):本帧媒体字节(`output` 媒体块的 `data` 与 `structured` 媒体卡的 `file.base64` 一起计)+ 这条连接 / 这个订阅者还没送出去的字节 > 上界(64 MiB)⇒ 本帧媒体降成持久面那只形(媒体块换描述符 `{ type, mimeType?, bytes, truncated: true }`,帧级 `truncated` + `totalChars`;媒体卡去掉 `file.base64`)。每降一帧记一次,只在这只函数里记(三处写口不各记)。放行的最坏后果 = 一个读得慢的观众在 SSE 上看不到这张图,只见「这里有一张多大的什么图、没送达」(账本面本就恒是描述符),不是门、不是凭据、不改任何裁决;反方向(不降)让一条停读的连接随帧数线性压住几百 MiB。帧上它与引擎自己的媒体预算降级同形不可分,所以计数是运维唯一能看出「多少观众丢了图」的位;计数持续非零 = 有消费端读流太慢(或网络瓶颈),去看那几条连接。detail = `backlog=<待发字节> media=<本帧媒体字节>`。",
26
26
  },
27
27
  "server.memory-consolidation.operator-valve-unjudged": {
28
28
  cls: "P-DEBT",
@@ -19,36 +19,66 @@
19
19
  * entry). Canonical casing = the first-seen key (process.env's native casing wins since the
20
20
  * inherit base is spread first).
21
21
  * - layering = env COMPOSITION is a pure function of config + process.env and never reads the resolved shell;
22
- * the shell belongs to the SPAWN layer only ({@link pinHostShell}, applied by the spawn sites after
23
- * `shellReady()`). A read surface (`cdPath`) therefore never depends on resolution having settled.
22
+ * the shell belongs to the SPAWN layer only ({@link pinHostShell}). A read surface (`cdPath`) therefore
23
+ * never depends on resolution having settled.
24
+ * - order = EVERY spawn site (the four exec legs behind `shellReady()`, both hook legs) awaits
25
+ * {@link resolveHostShell} itself before it reads {@link hostShell} — no spawn depends on a warm-up or on
26
+ * the boot step below having run first.
27
+ * - signal = the win32 resolver's STATE TRANSITIONS are the one operator signal source ({@link settleHostShellAtBoot}):
28
+ * one `warn host_shell_unavailable` on (unsettled | resolved) → failed, one `info host_shell_resolved` on
29
+ * (unsettled | failed) → resolved; the same state again prints nothing. No spawn site logs on its own.
24
30
  *
25
31
  * ⚠️ The win32 branches are UNVERIFIED on a real machine until S5 (Windows CI runner) — design D5 discipline:
26
32
  * structural tests only on mac/Linux; behavior-level bite happens on the first Windows-runner green.
27
33
  */
28
34
  import type { ChildProcess } from "node:child_process";
29
35
  import { killProcessTree, type BackgroundHardKillOutcome } from "@sema-agent/core";
36
+ import type { Logger } from "../observability/logger.js";
30
37
  export declare const IS_WIN32: boolean;
31
38
  export interface HostShell {
32
39
  shell: string;
33
40
  args: string[];
34
41
  }
42
+ /** Where the transition lines go. Installed ONCE by {@link settleHostShellAtBoot}; absent (tests, embedders that never
43
+ * boot the lane) ⇒ the resolver is silent and every failure still reaches its caller as a thrown cause. */
44
+ type HostShellLog = Pick<Logger, "info" | "warn">;
35
45
  /**
36
- * Resolve the host shell ONCE per process (cached). POSIX resolves synchronously to `/bin/sh` — zero behavior
37
- * change. win32 defers to core's `getShellConfig` (explicit `HOST_SHELL_PATH` override → Git-for-Windows
46
+ * Resolve the host shell ONCE per process (cached). POSIX resolves via core's `getShellConfig` with an `sh` fallback —
47
+ * zero behavior change. win32 defers to core's `getShellConfig` (explicit `HOST_SHELL_PATH` override → Git-for-Windows
38
48
  * install dirs → PATH `bash.exe` with the WSL-launcher filter) and THROWS a self-explaining error when no
39
49
  * usable bash exists (design D1 fail-loud: "install Git for Windows", never a silent cmd fallback).
40
50
  *
41
- * Call sites: `shellReady()` awaits this once (all exec/bg/stream spawns are behind it), then the spawn sites
42
- * read the cached value synchronously via {@link hostShell}.
51
+ * win32 is SINGLE-FLIGHT: callers that arrive while a probe is in flight (the boot step, a first-Bash storm, the hook
52
+ * legs) share that one probe, so no late probe can overwrite what an earlier one settled — a resolved shell is
53
+ * terminal for the process, a failed probe is forgotten the moment it settles (the next call probes again).
54
+ *
55
+ * Call sites: every spawn site awaits this itself, then reads the cached value synchronously via {@link hostShell} —
56
+ * the exec legs through `shellReady()`, the hook legs directly — and {@link settleHostShellAtBoot} awaits it once at
57
+ * assembly so the first transition line lands before any user command does.
43
58
  */
44
59
  export declare function resolveHostShell(): Promise<HostShell>;
45
- /** Synchronous read of the resolved shell — a SPAWN-layer read, valid only AFTER {@link resolveHostShell} settled
46
- * (the spawn sites are all behind `shellReady()`, which awaits it). It is never an env-composition or read-surface
47
- * input (see {@link pinHostShell}). POSIX never needs the async step.
48
- * win32, not settled: a resolve that FAILED throws that failure (the real cause + the way out, memoised above);
49
- * only a resolve that never ran / is still in flight reaches the ordering sentence — on the spawn paths that arm
50
- * is unreachable (they await the resolve first), so it stays as the loud guard against a future mis-ordered caller
51
- * (a programming bug, not an operator condition — fail loud, not /bin/sh-on-windows). */
60
+ /**
61
+ * Assembly step for a deployment that runs the HOST lane — called once per process by each entry (`settleHostLaneShell`
62
+ * in `boot/execution-lane.ts`, the only caller): install the log sink for the resolver's transitions, then settle the
63
+ * shell once so a missing Git Bash is announced at boot instead of at the first user's first Bash call.
64
+ *
65
+ * Never rejects: the failure comes back as DATA (`ok:false` + the cause) and does not stop the boot — the lane's other
66
+ * tools work, Bash refuses at spawn with the real cause, and win32 has already printed the transition line. POSIX has
67
+ * no "no shell" state to announce, so it prints nothing.
68
+ */
69
+ export declare function settleHostShellAtBoot(log: HostShellLog): Promise<{
70
+ ok: true;
71
+ shell: HostShell;
72
+ } | {
73
+ ok: false;
74
+ error: Error;
75
+ }>;
76
+ /** Synchronous read of the resolved shell — a SPAWN-layer read, valid only AFTER the caller awaited
77
+ * {@link resolveHostShell} (every spawn site does; see the `order` note in the header). It is never an env-composition
78
+ * or read-surface input (see {@link pinHostShell}). POSIX never needs the async step.
79
+ * win32: resolved ⇒ the shell; a resolve that FAILED ⇒ throws that failure (the real cause + the way out); never
80
+ * settled ⇒ throws a plain state sentence (no spawn site reaches it — each awaits first; it stays so that a future
81
+ * unawaited caller fails loud, never spawns /bin/sh-on-windows). */
52
82
  export declare function hostShell(): HostShell;
53
83
  /**
54
84
  * The SPAWN layer's one env edit: win32 pins `SHELL` to the resolved Git Bash (subshell scripts read `$SHELL`).
@@ -1,10 +1,34 @@
1
1
  import os from "node:os";
2
2
  import { getShellConfig, killProcessTree, signalProcessTree } from "@sema-agent/core";
3
+ import { redactSecrets } from "../trace/redact.js";
3
4
  export const IS_WIN32 = process.platform === "win32";
4
5
  const POSIX_SH_FALLBACK = { shell: "/bin/sh", args: ["-c"] };
5
- let cachedWinShell;
6
6
  let cachedPosixShell;
7
- let winShellFailure;
7
+ let winShellState = { kind: "unsettled" };
8
+ let winProbe;
9
+ const WIN_SHELL_REMEDY = "Install Git for Windows (https://gitforwindows.org) or set HOST_SHELL_PATH to a bash.exe. " +
10
+ "Note: System32\\bash.exe is the WSL launcher, not a Windows bash — it is deliberately rejected.";
11
+ class HostShellUnavailable extends Error {
12
+ detail;
13
+ constructor(custom, detail) {
14
+ super(`host lane needs a POSIX shell on Windows and none was found${custom ? ` (HOST_SHELL_PATH=${custom})` : ""}: ${detail}. ${WIN_SHELL_REMEDY}`);
15
+ this.detail = detail;
16
+ }
17
+ }
18
+ let hostShellLog;
19
+ let announcedKind;
20
+ function settleWinShell(next, detail) {
21
+ winShellState = next;
22
+ if (hostShellLog === undefined || announcedKind === next.kind)
23
+ return;
24
+ announcedKind = next.kind;
25
+ if (next.kind === "failed") {
26
+ hostShellLog.warn("host_shell_unavailable", { err: redactSecrets(detail ?? next.error.message).slice(0, 500), remedy: WIN_SHELL_REMEDY });
27
+ }
28
+ else {
29
+ hostShellLog.info("host_shell_resolved", { shell: next.shell.shell });
30
+ }
31
+ }
8
32
  export async function resolveHostShell() {
9
33
  if (!IS_WIN32) {
10
34
  if (cachedPosixShell)
@@ -13,31 +37,54 @@ export async function resolveHostShell() {
13
37
  cachedPosixShell = posixCfg.ok ? { shell: posixCfg.value.shell, args: posixCfg.value.args } : POSIX_SH_FALLBACK;
14
38
  return cachedPosixShell;
15
39
  }
16
- if (cachedWinShell)
17
- return cachedWinShell;
40
+ if (winShellState.kind === "resolved") {
41
+ settleWinShell(winShellState);
42
+ return winShellState.shell;
43
+ }
44
+ winProbe ??= probeWinShell().finally(() => {
45
+ winProbe = undefined;
46
+ });
47
+ return winProbe;
48
+ }
49
+ async function probeWinShell() {
18
50
  const custom = process.env.HOST_SHELL_PATH;
51
+ let shell;
19
52
  try {
20
53
  const cfg = await getShellConfig(custom || undefined);
21
- if (!cfg.ok) {
22
- throw new Error(`host lane needs a POSIX shell on Windows and none was found${custom ? ` (HOST_SHELL_PATH=${custom})` : ""}: ` +
23
- `${cfg.error.message}. Install Git for Windows (https://gitforwindows.org) or set HOST_SHELL_PATH to a bash.exe. ` +
24
- `Note: System32\\bash.exe is the WSL launcher, not a Windows bash — it is deliberately rejected.`);
25
- }
26
- cachedWinShell = { shell: cfg.value.shell, args: cfg.value.args };
27
- return cachedWinShell;
54
+ if (!cfg.ok)
55
+ throw new HostShellUnavailable(custom, cfg.error.message);
56
+ shell = { shell: cfg.value.shell, args: cfg.value.args };
28
57
  }
29
58
  catch (e) {
30
- winShellFailure = e instanceof Error ? e : new Error(String(e));
31
- throw winShellFailure;
59
+ const error = e instanceof Error ? e : new Error(String(e));
60
+ settleWinShell({ kind: "failed", error }, e instanceof HostShellUnavailable ? e.detail : undefined);
61
+ throw error;
62
+ }
63
+ settleWinShell({ kind: "resolved", shell });
64
+ return shell;
65
+ }
66
+ export async function settleHostShellAtBoot(log) {
67
+ hostShellLog = log;
68
+ try {
69
+ return { ok: true, shell: await resolveHostShell() };
70
+ }
71
+ catch (e) {
72
+ return { ok: false, error: e instanceof Error ? e : new Error(String(e)) };
32
73
  }
33
74
  }
34
75
  export function hostShell() {
35
76
  if (!IS_WIN32) {
36
77
  return cachedPosixShell ?? POSIX_SH_FALLBACK;
37
78
  }
38
- if (cachedWinShell)
39
- return cachedWinShell;
40
- throw winShellFailure ?? new Error("host shell not resolved yet — resolveHostShell() must settle before any spawn (shellReady ordering bug)");
79
+ const state = winShellState;
80
+ switch (state.kind) {
81
+ case "resolved":
82
+ return state.shell;
83
+ case "failed":
84
+ throw state.error;
85
+ case "unsettled":
86
+ throw new Error("host shell has not been resolved in this process: await resolveHostShell() before reading it");
87
+ }
41
88
  }
42
89
  export function pinHostShell(env, sh) {
43
90
  if (IS_WIN32)
@@ -45,9 +92,11 @@ export function pinHostShell(env, sh) {
45
92
  return env;
46
93
  }
47
94
  export function resetHostShellCacheForTest() {
48
- cachedWinShell = undefined;
95
+ winShellState = { kind: "unsettled" };
96
+ winProbe = undefined;
49
97
  cachedPosixShell = undefined;
50
- winShellFailure = undefined;
98
+ hostShellLog = undefined;
99
+ announcedKind = undefined;
51
100
  }
52
101
  export function spawnGroupOptions() {
53
102
  return IS_WIN32 ? { detached: false, windowsHide: true } : { detached: true };
@@ -7,7 +7,7 @@ import { openSync, closeSync, readSync, statSync, truncateSync, unlinkSync, mkdi
7
7
  import { StringDecoder } from "node:string_decoder";
8
8
  import { armPipeDestroyGrace, numEnvOr, SYMLINK_CHAIN_MAX_HOPS } from "./remote-shell.js";
9
9
  import { hostBackgroundShellEnabled, hostExecSpoolEnabled } from "../config.js";
10
- import { resolveHostShell, hostShell, pinHostShell, spawnGroupOptions, killTreeHard, killTreeSoft, hardKillGroupNow, collapseWin32EnvKeys, adoptHostForegroundChild } from "./host-platform.js";
10
+ import { IS_WIN32, resolveHostShell, hostShell, pinHostShell, spawnGroupOptions, killTreeHard, killTreeSoft, hardKillGroupNow, collapseWin32EnvKeys, adoptHostForegroundChild } from "./host-platform.js";
11
11
  import { BackgroundShellManager, seedMemStream, feedMemStream, drainMemStream } from "./background-shell-support.js";
12
12
  import { FileError, NodeExecutionEnv, ExecutionError, RemoteExecutionError, RollingTailBuffer, markTruncated, SchedulerError, BackgroundShellError, } from "@sema-agent/core";
13
13
  import { allowlistedEnv, composeHostSubprocessEnv, hostSubprocessEnv } from "../server-secret-env.js";
@@ -395,7 +395,7 @@ export class RemoteHostExecutionEnv {
395
395
  const cwd = this.resolve(options?.cwd ?? this.cwd);
396
396
  const timeoutMs = options?.timeout != null ? options.timeout * 1000 : this.cfg.commandTimeoutMs;
397
397
  const detachSignal = options?.detachSignal;
398
- if (process.platform !== "win32" && hostExecSpoolEnabled()) {
398
+ if (!IS_WIN32 && hostExecSpoolEnabled()) {
399
399
  const spool = this.openFgSpool();
400
400
  if (spool)
401
401
  return this.execViaSpool(command, options, cwd, timeoutMs, detachSignal, spool);
@@ -1114,7 +1114,7 @@ export class RemoteHostExecutionEnv {
1114
1114
  break;
1115
1115
  seen.add(next);
1116
1116
  hops.push(next);
1117
- if (process.platform === "win32" && UNC_SPELLING.test(next))
1117
+ if (IS_WIN32 && UNC_SPELLING.test(next))
1118
1118
  break;
1119
1119
  cur = next;
1120
1120
  }
package/dist/run-local.js CHANGED
@@ -25,10 +25,10 @@ import { acquireLocalBackendWithWait } from "./plugins/store-lock-wait.js";
25
25
  import { configLocalRoot, configProviderKind, createConfigProvider } from "./config-provider.js";
26
26
  import { CONFIG_DOMAIN_INVALID_NOTE, buildCandidateRejectedFields, judgeConfigCandidate } from "./web-admin-client/candidate-gate.js";
27
27
  import { buildUnreadConfigFileFields } from "./web-admin-client/read-warnings.js";
28
- import { composeExecutionLane } from "./boot/execution-lane.js";
28
+ import { composeExecutionLane, settleHostLaneShell } from "./boot/execution-lane.js";
29
29
  import { judgeConsolidationValve } from "./boot/memory-consolidation.js";
30
30
  import { logConfigDirMigration, migrateLocalConfigDirectory } from "./config-d-migration.js";
31
- import { applyEffective, applyCenterReadFace, resolveMcpServers, mcpForScenario, resolveA2aPeers, a2aForScenario } from "./web-admin-client/facade.js";
31
+ import { applyEffective, applyCenterReadFace, applySkillsManifest, resolveMcpServers, mcpForScenario, resolveA2aPeers, a2aForScenario } from "./web-admin-client/facade.js";
32
32
  import { hostExecutionEnvFactory } from "./plugins/remote-env-host.js";
33
33
  import { executionLaneHomeDir, executionLaneOf } from "./execution-lane-caps.js";
34
34
  import { makeLoadProjectMemory, makeProbeInstructionSources } from "./project-memory.js";
@@ -436,6 +436,7 @@ export async function runLocal(argv, deps = {}) {
436
436
  const keyResolver = createKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys);
437
437
  const hostRx = config.remoteExec?.provider === "host" ? config.remoteExec : undefined;
438
438
  const workspaceDir = args.workspace ?? hostRx?.workspaceBase ?? process.cwd();
439
+ await settleHostLaneShell(config, logger);
439
440
  const executionEnvFactory = hostRx
440
441
  ? hostExecutionEnvFactory({
441
442
  workspaceDir,
@@ -595,7 +596,8 @@ export async function runLocal(argv, deps = {}) {
595
596
  const subRunnerDeps = { ...sharedRunnerDeps, sessionStore: new ForkRoutingSessionStore(sessionStore, new TtlSessionStore({ defaultTtlDays: 1 / 24 })), onError: engineOnError, onAsk };
596
597
  const subRunner = new Runner(subRunnerDeps);
597
598
  const handslessRunner = new Runner(withoutHands(runnerDeps));
598
- const skills = loadDeploymentSkills(config);
599
+ const deploymentSkills = loadDeploymentSkills(config);
600
+ const skills = effective?.skills ? await applySkillsManifest(deploymentSkills, effective.skills, configProvider, logger) : deploymentSkills;
599
601
  const repoClient = config.gitApiBaseUrl ? createRepoClient(config.gitApiKind, config.gitApiBaseUrl, config.gitApiToken) : undefined;
600
602
  const webSearchCfg = webSearchConfigFromEnv();
601
603
  const webSearch = webSearchCfg ? createWebSearchBackend(webSearchCfg) : undefined;
@@ -23,6 +23,7 @@
23
23
  * When the gate is closed, a sent `mcpServers` is IGNORED (advertised via `capabilities.mcpInjection`).
24
24
  */
25
25
  import { type CompliancePosture, type EngineNotice, type LockedKey, type McpInjectionDropReason, type McpServerSpec } from "@sema-agent/core";
26
+ import { type McpIdentityHeadersIssue, type RequestMcpServerSpec } from "./mcp-assertion-header.js";
26
27
  /** Bound on caller-supplied servers (anti-DoS; a real local `.mcp.json` has a handful). */
27
28
  export declare const MAX_REQUEST_MCP_SERVERS = 32;
28
29
  /**
@@ -53,6 +54,8 @@ export declare const MAX_REQUEST_MCP_DROP_NAMES = 32;
53
54
  * ## 🔴 两向钉在 core 的 `McpServerSpec` 上(不是手抄)
54
55
  * 下面六道编译期门:型上多一个键而表没跟 ⇒ 红;表里写一个型上没有的词(改名 / 拼错 / 删键后的残条目)
55
56
  * ⇒ 也红。于是这三张表是从 core 的型**派生**出来的数据,而不是誰记得去同步的清单。
57
+ * (唯一一格不在 core 型上的键 = http 传输的 `assertionHeader`:引擎尚无座,本仓承载 —— 那一支的门钉在
58
+ * `mcp-assertion-header.ts` 的 {@link RequestMcpServerSpec} 上,引擎一旦长出同名座,那里的退役门当场红。)
56
59
  *
57
60
  * ## 两个消费点的处置**刻意不同**(同一张表,不同的门)
58
61
  * · 提交腿({@link normalizeServer})对词表外的键**宽容**(读不认识的键就当没有)—— 那条腿要能安全地
@@ -64,8 +67,9 @@ export declare const MAX_REQUEST_MCP_DROP_NAMES = 32;
64
67
  export declare const MCP_SERVER_ENTRY_KEYS: readonly ["allowTools", "contentOrigin", "elicitation", "name", "source", "toolAxes", "toolFaces", "transport"];
65
68
  /** `transport.kind:"stdio"` 的键闭集(两向钉在 core 那一支上)。 */
66
69
  export declare const STDIO_MCP_TRANSPORT_KEYS: readonly ["args", "command", "env", "kind"];
67
- /** `transport.kind:"http"` 的键闭集(同上)。 */
68
- export declare const HTTP_MCP_TRANSPORT_KEYS: readonly ["headers", "kind", "principalHeader", "url"];
70
+ /** `transport.kind:"http"` 的键闭集(同上)。`assertionHeader` 是本仓承载、引擎尚无座的一格(见 {@link RequestMcpServerSpec}):
71
+ * 表两向钉在**请求面的 http 传输型**上 = 引擎的键 ∪ 这一格;引擎一旦长出同名座,`mcp-assertion-header.ts` 的退役门当场红。 */
72
+ export declare const HTTP_MCP_TRANSPORT_KEYS: readonly ["assertionHeader", "headers", "kind", "principalHeader", "url"];
69
73
  /** 条目层:`{}` 与缺席等价的可选 Record 键。 */
70
74
  export declare const MCP_ENTRY_EMPTY_RECORD_KEYS: readonly ["toolAxes", "toolFaces"];
71
75
  /** 条目层:`[]` 与缺席等价的可选数组键。 */
@@ -113,7 +117,7 @@ export interface McpInjectionDropSeat {
113
117
  */
114
118
  export declare function validateRequestMcp(raw: unknown): ValidatedRequestMcp | null;
115
119
  type ValidatedRequestMcp = {
116
- ok: McpServerSpec[];
120
+ ok: RequestMcpServerSpec[];
117
121
  dropped: McpInjectionDrop[];
118
122
  droppedTotal: number;
119
123
  badContentOrigin: Array<{
@@ -180,6 +184,30 @@ export declare function assertRequestMcpUnlocked(bodyMcp: unknown, lockedKeys: R
180
184
  * 形不合(非数组 / 条目非对象)**不归本门**——那是 `validateRequestMcp` 的既有宽容形,本门不抢裁决。
181
185
  */
182
186
  export declare function assertRequestMcpContentOrigin(bodyMcp: unknown): void;
187
+ /**
188
+ * S-1515 —— `mcpServers[].transport`(http)上两个身份头的**值域门**(fresh 腿限定,同步 400):`principalHeader` 与 `assertionHeader`
189
+ * **同设**(`request.body_shape`),或 `assertionHeader` 在场而**不是字符串**(`request.field_invalid`)。
190
+ *
191
+ * 前者:两个键回答同一个问题(「这台 http 服务器的请求上用哪个头送身份」),同设 = 两个答案。settings 面由契约包在解析期判拒;
192
+ * 请求面在这里判拒 —— **同一条规则、两处执行者**,判据与拒句单点在 `mcp-assertion-header.ts`(对契约包原句逐字,差分钉守)。
193
+ * 后者:契约包的 zod 类型拒它;请求面若把它读成"没写过",一条鉴权声明就无声消失、server 却照常挂上去连。
194
+ *
195
+ * ## 位置与 leg 分流(与 {@link assertRequestMcpContentOrigin} 同款理由,不发明第二套)
196
+ * 排在腿首、任何副作用之前;**只在 fresh 腿**:`checkpoint_ctx` 存的是原始 `TaskRequestBody`,耐久 resume 逐字重放它,
197
+ * 从这里逃出去的 `HttpError` 会被折成 `409 resume_blocked_by_policy`,把一条 park 任务永久砖死。resume 腿走读器
198
+ * ({@link normalizeServer}):**降级到升级前的读法**(server 照挂、`principalHeader` 胜、`assertionHeader` 不生效)+ `classifyHonored` 一行 warn ——
199
+ * 不拒、也不摘 server。判据是纯值域、与租户车道无关(多租户腿本来就忽略整只数组,告诉调用方「你这条写错了」只多不少)。
200
+ */
201
+ export declare function assertRequestMcpIdentityHeaders(bodyMcp: unknown): void;
202
+ /** 一条**原始**请求条目上两个身份头的问题类(没有问题 / 不是对象 / 不是 http 传输 ⇒ `undefined`)—— 提交腿的门、探测口、重放腿的披露三处共读,
203
+ * 判据本体在 `mcp-assertion-header.ts`;这里只负责把「条目 → 传输对象」那一步读出来。 */
204
+ export declare function requestEntryIdentityIssue(entry: unknown): McpIdentityHeadersIssue | undefined;
205
+ /** 问题类 → wire 拒面(码 + 句子):两扇拒绝门(提交腿 / 探测口)同一份。条目名进句子,过唯一的回显清洗(剥控制 / 双向字符 + 截长);
206
+ * 读不出名字的条目用与丢弃点同一个占位。 */
207
+ export declare function requestEntryIdentityRefusal(entry: unknown): {
208
+ errorCode: "request.body_shape" | "request.field_invalid";
209
+ message: string;
210
+ } | undefined;
183
211
  /**
184
212
  * The ONE predicate for "this MCP server name belongs to the deployment": the scenario baseline (what mounted for
185
213
  * this scenario) ∪ every name the deployment DECLARED (`CenterMcp.reservedNames` — mounted or not, any scenario).