@sema-agent/server 7.129.1 → 7.131.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 +102 -0
- package/README.md +8 -5
- package/README.zh-CN.md +6 -4
- package/USAGE.md +48 -9
- package/dist/audit.js +4 -1
- package/dist/bench/s1/live-deps.js +2 -0
- package/dist/boot/resolve-spec.js +43 -16
- package/dist/boot/stage-02-web-admin-pull.js +9 -1
- package/dist/boot/stage-10-http-server.d.ts +1 -1
- package/dist/boot/stage-10-http-server.js +7 -2
- package/dist/capabilities/builtin-tools.js +2 -1
- package/dist/capabilities/composite-members.d.ts +1 -0
- package/dist/capabilities/composite-members.js +1 -0
- package/dist/capabilities/hands-lane.js +2 -2
- package/dist/capabilities/host-tool-names.d.ts +17 -0
- package/dist/capabilities/host-tool-names.js +15 -0
- package/dist/capabilities/mcp-probe.js +19 -6
- package/dist/capabilities/repo-tools.js +4 -3
- package/dist/capabilities/scenarios.d.ts +34 -0
- package/dist/capabilities/scenarios.js +20 -4
- package/dist/capabilities/select-environment-tool.js +2 -1
- package/dist/capabilities/send-user-file-tool.d.ts +0 -1
- package/dist/capabilities/send-user-file-tool.js +2 -2
- package/dist/capabilities/skills.js +1 -1
- package/dist/capabilities/tool-defer.js +2 -1
- package/dist/config-catalog.js +16 -5
- package/dist/config-invariants.js +2 -2
- package/dist/config-provider.js +2 -2
- package/dist/config-types.d.ts +6 -0
- package/dist/config.d.ts +2 -2
- package/dist/config.js +18 -17
- package/dist/elicitation.d.ts +7 -0
- package/dist/elicitation.js +20 -5
- package/dist/exec-bounds.d.ts +6 -6
- package/dist/hooks/hook-llm.d.ts +11 -1
- package/dist/hooks/hook-llm.js +11 -14
- package/dist/hooks/hook-runner.d.ts +55 -9
- package/dist/hooks/hook-runner.js +86 -16
- package/dist/http/admission.js +2 -2
- package/dist/http/approval-sweeps.js +2 -2
- package/dist/http/checkpoint-ctx.d.ts +12 -11
- package/dist/http/checkpoint-ctx.js +4 -4
- package/dist/http/dispatch.js +1 -1
- package/dist/http/resume-legs.js +6 -5
- package/dist/http/routes/approvals-assistant.js +1 -1
- package/dist/http/routes/capabilities.js +13 -5
- package/dist/http/routes/runs.js +18 -14
- package/dist/http/routes/sessions.js +22 -10
- package/dist/http/routes/tasks.js +16 -11
- package/dist/http/scenario-read-face.d.ts +14 -0
- package/dist/http/scenario-read-face.js +5 -0
- package/dist/http/send.d.ts +10 -0
- package/dist/http/send.js +6 -0
- package/dist/http/server.d.ts +16 -4
- package/dist/http/server.js +2 -2
- package/dist/http/stored-body-replay.d.ts +10 -3
- package/dist/http/stored-body-replay.js +27 -6
- package/dist/http/wire-types.d.ts +18 -5
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/mcp-assertion-header.d.ts +2 -2
- package/dist/mcp-assertion-header.js +3 -3
- package/dist/model-entry-refusal.d.ts +1 -7
- package/dist/model-route-endpoint.d.ts +51 -11
- package/dist/model-route-endpoint.js +45 -23
- package/dist/model-route-gap.d.ts +5 -4
- package/dist/model-route-gap.js +1 -1
- package/dist/model-select.d.ts +58 -0
- package/dist/model-select.js +31 -0
- package/dist/namespaced-rule-name.d.ts +7 -4
- package/dist/namespaced-rule-name.js +4 -3
- package/dist/observability/fail-open.d.ts +16 -7
- package/dist/observability/fail-open.js +10 -2
- package/dist/plugins/local-session-store.js +2 -2
- package/dist/plugins/one-time-migrations.d.ts +21 -1
- package/dist/plugins/one-time-migrations.js +33 -1
- package/dist/plugins/remote-env-adb.js +5 -4
- package/dist/plugins/remote-env-local-docker.js +5 -4
- package/dist/plugins/remote-env-ssh.js +5 -4
- package/dist/question.d.ts +18 -9
- package/dist/question.js +21 -13
- package/dist/request-key-closure.d.ts +2 -2
- package/dist/request-key-closure.js +16 -9
- package/dist/rule-entry-list.d.ts +124 -17
- package/dist/rule-entry-list.js +140 -24
- package/dist/run-local.d.ts +3 -3
- package/dist/run-local.js +8 -7
- package/dist/runs.d.ts +42 -2
- package/dist/runs.js +45 -2
- package/dist/runtime-caps-resolver.d.ts +7 -0
- package/dist/runtime-caps-resolver.js +1 -0
- package/dist/session-titler.d.ts +4 -1
- package/dist/session-titler.js +13 -3
- package/dist/spec-fields.d.ts +8 -4
- package/dist/stored-request-hooks-m20.d.ts +23 -0
- package/dist/stored-request-hooks-m20.js +124 -0
- package/dist/stored-request-tool-axes-m19.d.ts +29 -0
- package/dist/stored-request-tool-axes-m19.js +27 -0
- package/dist/task-mcp.d.ts +55 -60
- package/dist/task-mcp.js +197 -96
- package/dist/task-settings.d.ts +41 -5
- package/dist/task-settings.js +39 -25
- package/dist/tool-axis-words.d.ts +14 -17
- package/dist/trace/core-keyset-guard.d.ts +2 -2
- package/dist/trace/ledger-sink.d.ts +29 -0
- package/dist/trace/ledger-sink.js +11 -0
- package/dist/trace/project.d.ts +6 -0
- package/dist/trace/project.js +11 -0
- package/dist/trace/wire-projection-faces.d.ts +1 -1
- package/dist/trace/wire-projection-faces.js +1 -1
- package/dist/user-authored-text.d.ts +7 -0
- package/dist/user-authored-text.js +3 -0
- package/dist/web-admin-client/apply-effective.js +10 -5
- package/dist/web-admin-client/skills-mcp.d.ts +1 -1
- package/dist/web-admin-client/types.d.ts +1 -1
- package/package.json +2 -2
- package/scripts/drain-check-local.mjs +3 -2
package/dist/task-mcp.d.ts
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import { type CompliancePosture, type EngineNotice, type LockedKey, type McpInjectionDropReason, type McpServerSpec } from "@sema-agent/core";
|
|
26
26
|
import { type McpIdentityHeadersIssue, type RequestMcpServerSpec } from "./mcp-assertion-header.js";
|
|
27
|
+
import type { StoredToolAxesRereadM19 } from "./stored-request-tool-axes-m19.js";
|
|
27
28
|
/** Bound on caller-supplied servers (anti-DoS; a real local `.mcp.json` has a handful). */
|
|
28
29
|
export declare const MAX_REQUEST_MCP_SERVERS = 32;
|
|
29
30
|
/**
|
|
@@ -63,6 +64,10 @@ export declare const MAX_REQUEST_MCP_DROP_NAMES = 32;
|
|
|
63
64
|
* · 探测口**响亮拒**(400,点名 `mcpServers[i].<键>`)—— 它问的是「这台到底能不能用」,把一个拼错的键
|
|
64
65
|
* 悄悄忽略再回一份好看的名单,正是那条面要根除的假答案。
|
|
65
66
|
* 两个门,一个词表。表在这里,门在各自的腿上。
|
|
67
|
+
*
|
|
68
|
+
* ## 受理键的判官表也钉在这张表上
|
|
69
|
+
* fresh 提交腿对**受理键的值**同样响亮拒({@link assertRequestMcpEntries}):逐键判官表 {@link ENTRY_KEY_JUDGES} 是 `{ [K in 本表的键]: 判官 }` 的映射型 ——
|
|
70
|
+
* 本表多一个键而没写判官 ⇒ 编译红,不会出现「这个键写坏了却被悄悄受理」的新键。词表外的键仍由读器宽容、由探测口闭集拒(上面两条不变)。
|
|
66
71
|
*/
|
|
67
72
|
export declare const MCP_SERVER_ENTRY_KEYS: readonly ["allowTools", "contentOrigin", "elicitation", "name", "source", "toolAxes", "toolFaces", "transport"];
|
|
68
73
|
/** `transport.kind:"stdio"` 的键闭集(两向钉在 core 那一支上)。 */
|
|
@@ -114,6 +119,7 @@ export interface McpInjectionDropSeat {
|
|
|
114
119
|
* Shape-check the raw `body.mcpServers`. `null` → a non-array (caller error; the route may 400). Otherwise the
|
|
115
120
|
* valid subset + the names of dropped (malformed/over-cap) entries + [ref]'s `badContentOrigin`
|
|
116
121
|
* (**降级**掉的内容出处声明,与 `dropped` 是两回事:那条 server **留着**,丢的只是它的声明)。
|
|
122
|
+
* 这一口没有 M19 的重读座(探测口与单测读到的 `toolAxes` 恒是判官的读数);重读只在续跑重放腿经 {@link resolveRequestMcp} 发生。
|
|
117
123
|
*/
|
|
118
124
|
export declare function validateRequestMcp(raw: unknown): ValidatedRequestMcp | null;
|
|
119
125
|
type ValidatedRequestMcp = {
|
|
@@ -143,71 +149,57 @@ type ValidatedRequestMcp = {
|
|
|
143
149
|
* 任务。同样是失败,同步 400 至少是**立刻可诊断**的那一种,而且拒得出理由(锁在哪、怎么改)。
|
|
144
150
|
*/
|
|
145
151
|
export declare function assertRequestMcpUnlocked(bodyMcp: unknown, lockedKeys: ReadonlySet<LockedKey>): void;
|
|
146
|
-
/**
|
|
147
|
-
* [ref](core 5.60.0 提货批件②)—— 请求体里 `mcpServers[].contentOrigin` 的**值域门**,腿首同步拒。
|
|
148
|
-
*
|
|
149
|
-
* ## 先把 core 的硬条款抄下来,因为本腿的全部争议都在它上面
|
|
150
|
-
* `McpServerSpec.contentOrigin` 顶注逐字:「TRUST SOURCE — a DEPLOYMENT-plane key … any assembly layer
|
|
151
|
-
* that accepts REQUEST-side MCP entries **must reject or strip this key** from them … A single-tenant
|
|
152
|
-
* superuser surface (a host reading its own `--mcp-config` file) **IS the deployment plane and needs no
|
|
153
|
-
* such gate**.」
|
|
154
|
-
*
|
|
155
|
-
* ## 本仓怎么兑现这条义务(**结构性**,不是靠纪律)
|
|
156
|
-
* 请求腿的条目只有在 {@link mcpInjectionHonored} 为真时才被读进 `TaskSpec.mcp`,而那只谓词的语义正是
|
|
157
|
-
* 「本部署是单用户 ⇒ 请求者就是这台 worker 的超管」—— core 点名豁免的那一格。多租户部署
|
|
158
|
-
* (`requirePrincipal===true`)整只 `mcpServers` 被忽略,租户**结构上**够不着这个键(钉见
|
|
159
|
-
* `test/task-mcp.test.ts` 的多租户格)。同一条腿今天已经透传 `toolAxes`(喂**审批门**,权限比本键**高**
|
|
160
|
-
* 一档 —— core 明写 contentOrigin「no gate/policy/roster effect」)与 stdio 的**任意命令**(worker 宿主
|
|
161
|
-
* 上的真无限制执行);在那两样之后,「我声明我自己这台 server 的内容不算外来」不构成任何新的暴露面。
|
|
162
|
-
* 名字撞车时 baseline 恒赢(`mergeRequestMcp`)⇒ 调用方也改写不了一台**已配置** server 的声明。
|
|
163
|
-
*
|
|
164
|
-
* ## 为什么 fresh 腿是 400 而不是静默丢键
|
|
165
|
-
* 静默丢键会把一次**信任边界声明**变成探针 —— 调用方以为声明生效了,其实没有。本仓 `opts.leg` 的成文
|
|
166
|
-
* 契约(`ServiceDeps.resolveSpec` 顶注)逐字写着 fresh 腿「may fail-loud 400 on combos that would
|
|
167
|
-
* silently discard caller payload」,这正是那种 combo。
|
|
168
|
-
*
|
|
169
|
-
* ## 🔴 但只在 fresh 腿(与 {@link assertRequestMcpUnlocked} **刻意分歧**)
|
|
170
|
-
* 锁那条敢在 resume 腿同拒,是因为它有一个本门**没有**的前提:锁在场 ⇒ core 的 preflight 无论如何都会
|
|
171
|
-
* 拒这条腿,放进去只换来一个跑起来又终态失败的任务。而坏 `contentOrigin` 在本仓两个平面都被**降级**掉了
|
|
172
|
-
* (`normalizeServer` / `resolveMcpServers`),core 根本看不到它,也就永远不会拒。
|
|
173
|
-
* 与此同时 `checkpoint_ctx` 存的是**原始** `TaskRequestBody`,耐久 resume 逐字重放它,从本门逃出去的
|
|
174
|
-
* `HttpError` 会被 `src/http/server.ts` 折成 `409 resume_blocked_by_policy` ⇒ 一条**升级前** park、body 里
|
|
175
|
-
* 带着当时被静默忽略的坏值的任务,升级后**永久**恢复不了(codex 交叉复审 R1-[high],验真后采纳)。
|
|
176
|
-
* ⇒ 照本仓成文的三处 `opts.leg` 先例(`appendSystemPrompt` 案 / `@model` 名单 [ref] 终裁 (a) /
|
|
177
|
-
* `compactionModel` 案,都逐字写着「砖死 resume 比降级糟」):fresh fail-loud,resume 降级 + 留声
|
|
178
|
-
* (留声点 = {@link resolveRequestMcp} 的 `mcp_content_class_dropped`)。挂点判据在 `boot/resolve-spec.ts`。
|
|
179
|
-
*
|
|
180
|
-
* ## 位置与射程
|
|
181
|
-
* 排在腿首、`assertRequestMcpUnlocked` 旁边 —— **任何副作用之前**(附件绑定 / per-session cwd·env 写入;
|
|
182
|
-
* 理由逐字见 `boot/resolve-spec.ts` 那两行的注)。判据是**纯值域**、与**租户车道**无关:一个写坏的请求
|
|
183
|
-
* 在哪条车道上都是写坏的,而多租户腿本来就要忽略整只数组,告诉调用方「你这个键写错了」只多不少。
|
|
184
|
-
* 形不合(非数组 / 条目非对象)**不归本门**——那是 `validateRequestMcp` 的既有宽容形,本门不抢裁决。
|
|
185
|
-
*/
|
|
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
152
|
/** 一条**原始**请求条目上两个身份头的问题类(没有问题 / 不是对象 / 不是 http 传输 ⇒ `undefined`)—— 提交腿的门、探测口、重放腿的披露三处共读,
|
|
203
153
|
* 判据本体在 `mcp-assertion-header.ts`;这里只负责把「条目 → 传输对象」那一步读出来。 */
|
|
204
154
|
export declare function requestEntryIdentityIssue(entry: unknown): McpIdentityHeadersIssue | undefined;
|
|
205
|
-
/** 问题类 → wire 拒面(码 + 句子):两扇拒绝门(提交腿 / 探测口)
|
|
206
|
-
*
|
|
207
|
-
export declare function requestEntryIdentityRefusal(entry: unknown): {
|
|
155
|
+
/** 问题类 → wire 拒面(码 + 句子):两扇拒绝门(提交腿 / 探测口)同一份。`both_set` 是键**组合**不合法(`request.body_shape`),句子对契约包解析期原句逐字,条目名过唯一的回显清洗
|
|
156
|
+
* (剥控制 / 双向字符 + 截长;读不出名字的条目用与丢弃点同一个占位);`assertionHeader` 非串是该键的值不合法,与其余所有受理键同一个拒形({@link entryFieldRefusal},点名 `<at>.transport.assertionHeader`)。 */
|
|
157
|
+
export declare function requestEntryIdentityRefusal(entry: unknown, at: string): {
|
|
208
158
|
errorCode: "request.body_shape" | "request.field_invalid";
|
|
209
159
|
message: string;
|
|
210
160
|
} | undefined;
|
|
161
|
+
/**
|
|
162
|
+
* S-1629 —— 请求体 `mcpServers[i]` **每个受理键**的值域门(fresh 腿限定,同步 400):**一条律**,取代此前按键各开一扇的三道门
|
|
163
|
+
* (`contentOrigin` / 身份头 / `toolAxes`)与其余键上「整条悄悄摘掉」「悄悄丢键」两种沉默。
|
|
164
|
+
*
|
|
165
|
+
* ## 律
|
|
166
|
+
* fresh 提交腿上,条目的**任一受理键**写坏 ⇒ `400 request.field_invalid`,拒句点名 `mcpServers[i].<键路径>`、server 名与**该键判官的原因句**。
|
|
167
|
+
* 不挂一半、不丢声明、不默认:调用方是请求体作者,同一扇门对同一类错误只有一种答案。修前同一条条目上有三种(`toolAxes` / `contentOrigin` 坏 ⇒ 400;
|
|
168
|
+
* `allowTools` / `toolFaces` / `name` / `transport` 坏 ⇒ 200 + 整条 server 不进 + 运维 warn;`elicitation` / `source` / `transport.{args,env,headers,principalHeader}`
|
|
169
|
+
* 坏 ⇒ 200 + 悄悄丢键),而「两个键方向相反所以处置不同」只是读器**对存量 body** 的取舍({@link normalizeServer}),不是给在场调用方的答复该有的差别:
|
|
170
|
+
* 对一个 fresh 调用方,「收窄声明没生效」与「放宽声明没生效」同样是一份没有生效的声明,响亮拒比两种沉默都好。
|
|
171
|
+
*
|
|
172
|
+
* ## 每个键一只判官({@link ENTRY_KEY_JUDGES},表的键集两向钉在 `MCP_SERVER_ENTRY_KEYS` 上:新增受理键而没写判官 ⇒ 编译红)
|
|
173
|
+
* 判官**就是读器用的那一只**,不是第二份:`toolAxes` = 引擎根导出的 `readMcpToolAxes`;`toolFaces` = `readMcpToolFaces`;`contentOrigin` = `isContentOriginWord`;
|
|
174
|
+
* `source` = `isMcpSourceLabel`;`name` = `isUsableWireName`;`allowTools` / `transport.args` = {@link isStringArray};`transport.env` / `headers` = {@link isStringRecord};
|
|
175
|
+
* `elicitation` = {@link isElicitationFlag};`transport` 的硬字段(`kind` / `command` / `url`)与软字段(`args` / `env` / `headers` / `principalHeader`)= {@link readRequestTransport}
|
|
176
|
+
* (读器投影 transport 的那一只函数,门读它报的 `lost`)。两个身份头的**键组合**(同设 / `assertionHeader` 非串)仍归 `mcp-assertion-header.ts` 的判官({@link requestEntryIdentityRefusal},
|
|
177
|
+
* 码 `request.body_shape` 的那一句对契约包原句逐字),在逐键之前先判。
|
|
178
|
+
*
|
|
179
|
+
* ## 位置与 leg 分流(沿用本仓成文的 `opts.leg` 姿势,不发明第二套)
|
|
180
|
+
* 排在腿首、`assertRequestMcpUnlocked` 旁边 —— **任何副作用之前**(①阶段会把附件行持久绑定到 session,②阶段会写 per-session 的 cwd/env 表;一个最终 400 的请求若已经改掉这两样,
|
|
181
|
+
* 它就改变了后续被接受的任务会物化什么、在哪执行)。**只在 fresh 腿**:`checkpoint_ctx` 存的是**原始** `TaskRequestBody`,耐久 resume 逐字重放它,从这里逃出去的 `HttpError`
|
|
182
|
+
* 会被 `src/http/server.ts` 折成 `409 resume_blocked_by_policy` ⇒ 一条**升级前** park、body 里带着当时被受理的坏值的任务,升级后永久恢复不了
|
|
183
|
+
* (本仓三处 `opts.leg` 先例 —— `appendSystemPrompt` 案 / `@model` 名单 [ref] 终裁 (a) / `compactionModel` 案 —— 逐字写着「砖死 resume 比降级糟」)。
|
|
184
|
+
* 重放腿走读器({@link normalizeServer}),处置**逐字不变**:坏硬键(`name` / `transport` 硬字段 / `allowTools` / `toolAxes` / `toolFaces`)⇒ 这一条 server 整条不进 + 运维 warn
|
|
185
|
+
* `mcp_injection_dropped` + 用户面通告 `mcp.injection_dropped` 点名 `field`(`toolAxes` 在一次性迁移 M19 的窗开着时先按升级前读法重读一次,过了就挂,见读器的注);坏软键(`contentOrigin` / `elicitation` / `source` / `transport` 软字段)⇒ 丢键、server 照挂(`contentOrigin` 另有 `mcp_content_class_dropped` 留声)。
|
|
186
|
+
*
|
|
187
|
+
* ## 射程
|
|
188
|
+
* 判据是**纯值域**、与租户车道无关:一个写坏的请求在哪条车道上都是写坏的,而多租户腿本来就忽略整只数组,告诉调用方「你这个键写错了」只多不少。
|
|
189
|
+
* 只判前 `MAX_REQUEST_MCP_SERVERS` 条(能被挂载的那一段,与读器同一个切片)。不在本门的:条目本身不是对象 / `mcpServers` 不是数组(`validateRequestMcp` 的既有宽容形)、词表外的键(读器对未知键宽容,探测口才闭集)、超上限的尾项(`over_cap` 点名;它们不挂载,为一个永远不会生效的声明拒掉整份请求是新开一条拒绝面)。
|
|
190
|
+
* 关于 `contentOrigin` 的信任语义(core 把它定性为**部署面**键):请求腿的条目只有在 {@link mcpInjectionHonored} 为真(单用户 = 请求者就是这台 worker 的超管)时才被读进 `TaskSpec.mcp`,
|
|
191
|
+
* 多租户部署整只数组被忽略 ⇒ 租户**结构上**够不着这个键;名字撞车时 baseline 恒赢({@link mergeRequestMcp})⇒ 调用方也改写不了一台**已配置** server 的声明。
|
|
192
|
+
*/
|
|
193
|
+
export declare function assertRequestMcpEntries(bodyMcp: unknown): void;
|
|
194
|
+
/**
|
|
195
|
+
* 一条**原始**请求条目上 `toolAxes` 的拒面(码 + 句子);没有问题 / 条目不是对象 / 没写 `toolAxes` ⇒ `undefined`。探测口(`mcp-probe.ts` 的 `entryValueIssue`)在它自己的通用读器之前读这一只,
|
|
196
|
+
* 所以 `toolAxes` 在两扇拒绝门上是**同一句**:判官 = {@link ENTRY_KEY_JUDGES} 的 `toolAxes` 格(引擎的 `readMcpToolAxes`),句子 = {@link entryFieldRefusal}(与 fresh 提交腿的每个键同一个形)。
|
|
197
|
+
* `at` = 条目的键路径(`mcpServers[3]`)。
|
|
198
|
+
*/
|
|
199
|
+
export declare function requestEntryToolAxesRefusal(entry: unknown, at: string): {
|
|
200
|
+
errorCode: "request.field_invalid";
|
|
201
|
+
message: string;
|
|
202
|
+
} | undefined;
|
|
211
203
|
/**
|
|
212
204
|
* The ONE predicate for "this MCP server name belongs to the deployment": the scenario baseline (what mounted for
|
|
213
205
|
* this scenario) ∪ every name the deployment DECLARED (`CenterMcp.reservedNames` — mounted or not, any scenario).
|
|
@@ -247,6 +239,9 @@ export declare function resolveRequestMcp(baseline: McpServerSpec[] | undefined,
|
|
|
247
239
|
/** L-167:本条腿的**用户面**投递口。`sessionId` = 通告的路由半场(缺席 ⇒ 一条都不铸,见
|
|
248
240
|
* {@link mintInjectionDropNotices});`emit` 收下 core 铸好的通告,由调用腿决定投到哪(见 `boot/resolve-spec.ts`)。
|
|
249
241
|
* 整段缺席(桩、单测、非 HTTP 腿)⇒ 只走日志终点,行为逐字与本件之前相同。 */
|
|
250
|
-
notice?: McpInjectionDropSeat
|
|
242
|
+
notice?: McpInjectionDropSeat,
|
|
243
|
+
/** 一次性迁移 M19 的重读座(窗口见 `plugins/one-time-migrations.ts` 登记表):只在续跑重放腿、只在窗开着时由 `boot/resolve-spec.ts` 递进来;
|
|
244
|
+
* 缺席(新提交腿 / 窗关 / 桩)⇒ 判官判不过的 `toolAxes` 照原臂整条不进。 */
|
|
245
|
+
storedToolAxes?: StoredToolAxesRereadM19): McpServerSpec[] | undefined;
|
|
251
246
|
export {};
|
|
252
247
|
//# sourceMappingURL=task-mcp.d.ts.map
|
package/dist/task-mcp.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { mcpInjectionDroppedNotice, resolveComplianceDenies } from "@sema-agent/core";
|
|
2
|
-
import { isUsableWireName } from "./wire-name-bound.js";
|
|
1
|
+
import { mcpInjectionDroppedNotice, readMcpToolAxes, resolveComplianceDenies } from "@sema-agent/core";
|
|
2
|
+
import { describeBadWireName, isUsableWireName } from "./wire-name-bound.js";
|
|
3
3
|
import { HttpError, createCodedHttpError } from "./security.js";
|
|
4
4
|
import { GOVERNANCE_HTTP_STATUS } from "./governance-http-status.js";
|
|
5
5
|
import { CONTENT_ORIGIN_WORDS, describeBadContentOrigin, isContentOriginWord } from "./mcp-content-origin.js";
|
|
6
|
-
import { readMcpSourceLabel } from "./mcp-source-label.js";
|
|
6
|
+
import { MCP_SOURCE_LABEL_MAX, isMcpSourceLabel, readMcpSourceLabel } from "./mcp-source-label.js";
|
|
7
7
|
import { declaresMcpAssertionHeader, describeMcpAssertionHeaderNotAString, describeMcpIdentityHeadersConflict, mcpIdentityHeadersIssue, noteMcpAssertionHeaderIgnored, noteMcpAssertionHeaderNotSent, readMcpIdentityHeaders, toCoreMcpServerSpec } from "./mcp-assertion-header.js";
|
|
8
8
|
import { echoKeyName } from "./request-key-closure.js";
|
|
9
|
+
import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
|
|
9
10
|
import { readMcpToolFaces } from "./mcp-tool-faces.js";
|
|
10
|
-
import { isToolAxisIrreversibility, isToolEffect } from "./tool-axis-words.js";
|
|
11
11
|
export const MAX_REQUEST_MCP_SERVERS = 32;
|
|
12
12
|
export const MAX_REQUEST_MCP_DROP_NAMES = MAX_REQUEST_MCP_SERVERS;
|
|
13
13
|
export const MCP_SERVER_ENTRY_KEYS = ["allowTools", "contentOrigin", "elicitation", "name", "source", "toolAxes", "toolFaces", "transport"];
|
|
@@ -27,6 +27,80 @@ export function mcpInjectionHonored(config) {
|
|
|
27
27
|
function isStringRecord(v) {
|
|
28
28
|
return !!v && typeof v === "object" && !Array.isArray(v) && Object.values(v).every((x) => typeof x === "string");
|
|
29
29
|
}
|
|
30
|
+
function isRecord(v) {
|
|
31
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
32
|
+
}
|
|
33
|
+
function isStringArray(v) {
|
|
34
|
+
return Array.isArray(v) && v.every((a) => typeof a === "string");
|
|
35
|
+
}
|
|
36
|
+
function isElicitationFlag(v) {
|
|
37
|
+
return typeof v === "boolean";
|
|
38
|
+
}
|
|
39
|
+
function describeShape(v) {
|
|
40
|
+
if (v === undefined)
|
|
41
|
+
return "nothing (the key is absent)";
|
|
42
|
+
if (v === null)
|
|
43
|
+
return "null";
|
|
44
|
+
if (Array.isArray(v))
|
|
45
|
+
return "an array";
|
|
46
|
+
if (typeof v === "string")
|
|
47
|
+
return v.length === 0 ? "an empty string" : `a string of ${v.length} characters`;
|
|
48
|
+
if (typeof v === "object")
|
|
49
|
+
return "an object";
|
|
50
|
+
return `a ${typeof v}`;
|
|
51
|
+
}
|
|
52
|
+
function describeNotStringArray(v) {
|
|
53
|
+
return Array.isArray(v) ? "an array with a non-string element" : describeShape(v);
|
|
54
|
+
}
|
|
55
|
+
function describeNotStringRecord(v) {
|
|
56
|
+
return isRecord(v) ? "an object with a non-string value" : describeShape(v);
|
|
57
|
+
}
|
|
58
|
+
const transportBad = (path, reason) => ({ ok: false, issue: { path, reason } });
|
|
59
|
+
function readRequestTransport(t) {
|
|
60
|
+
if (!t || typeof t !== "object")
|
|
61
|
+
return transportBad("transport", `must be an object with a "kind" of "stdio" or "http" — got ${describeShape(t)}`);
|
|
62
|
+
const tr = t;
|
|
63
|
+
const lost = [];
|
|
64
|
+
if (tr.kind === "stdio") {
|
|
65
|
+
if (typeof tr.command !== "string" || tr.command.length === 0)
|
|
66
|
+
return transportBad("transport.command", `must be a non-empty string (the command to run) — got ${describeShape(tr.command)}`);
|
|
67
|
+
if (tr.args !== undefined && !isStringArray(tr.args))
|
|
68
|
+
lost.push({ path: "transport.args", reason: `must be an array of strings — got ${describeNotStringArray(tr.args)}` });
|
|
69
|
+
if (tr.env !== undefined && !isStringRecord(tr.env))
|
|
70
|
+
lost.push({ path: "transport.env", reason: `must be an object of string values — got ${describeNotStringRecord(tr.env)}` });
|
|
71
|
+
return {
|
|
72
|
+
ok: true,
|
|
73
|
+
transport: {
|
|
74
|
+
kind: "stdio",
|
|
75
|
+
command: tr.command,
|
|
76
|
+
...(isStringArray(tr.args) ? { args: tr.args } : {}),
|
|
77
|
+
...(isStringRecord(tr.env) ? { env: tr.env } : {}),
|
|
78
|
+
},
|
|
79
|
+
lost,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (tr.kind === "http") {
|
|
83
|
+
if (typeof tr.url !== "string" || !/^https?:\/\//i.test(tr.url))
|
|
84
|
+
return transportBad("transport.url", "must be an absolute http(s) URL (put credentials in transport.headers, never in the URL)");
|
|
85
|
+
const ids = readMcpIdentityHeaders(tr);
|
|
86
|
+
if (tr.headers !== undefined && !isStringRecord(tr.headers))
|
|
87
|
+
lost.push({ path: "transport.headers", reason: `must be an object of string values — got ${describeNotStringRecord(tr.headers)}` });
|
|
88
|
+
if (tr.principalHeader !== undefined && ids.principalHeader === undefined) {
|
|
89
|
+
lost.push({ path: "transport.principalHeader", reason: `must be a string (the HTTP header NAME the end-user principal is injected into) — got ${describeShape(tr.principalHeader)}` });
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
ok: true,
|
|
93
|
+
transport: {
|
|
94
|
+
kind: "http",
|
|
95
|
+
url: tr.url,
|
|
96
|
+
...(isStringRecord(tr.headers) ? { headers: tr.headers } : {}),
|
|
97
|
+
...ids,
|
|
98
|
+
},
|
|
99
|
+
lost,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return transportBad("transport.kind", `must be "stdio" or "http" — got ${describeShape(tr.kind)}`);
|
|
103
|
+
}
|
|
30
104
|
const bad = (field) => ({ ok: false, field });
|
|
31
105
|
const MALFORMED_NAME = "<malformed>";
|
|
32
106
|
function declaredName(e) {
|
|
@@ -34,65 +108,37 @@ function declaredName(e) {
|
|
|
34
108
|
? String(e.name)
|
|
35
109
|
: MALFORMED_NAME;
|
|
36
110
|
}
|
|
37
|
-
function normalizeServer(e, noteBadOrigin) {
|
|
111
|
+
function normalizeServer(e, noteBadOrigin, storedToolAxes) {
|
|
38
112
|
if (!e || typeof e !== "object")
|
|
39
113
|
return bad("entry");
|
|
40
114
|
const o = e;
|
|
41
115
|
if (!isUsableWireName(o.name))
|
|
42
116
|
return bad("name");
|
|
43
|
-
const
|
|
44
|
-
if (!
|
|
45
|
-
return bad(
|
|
46
|
-
const
|
|
47
|
-
let transport;
|
|
48
|
-
if (tr.kind === "stdio") {
|
|
49
|
-
if (typeof tr.command !== "string" || tr.command.length === 0)
|
|
50
|
-
return bad("transport.command");
|
|
51
|
-
transport = {
|
|
52
|
-
kind: "stdio",
|
|
53
|
-
command: tr.command,
|
|
54
|
-
...(Array.isArray(tr.args) && tr.args.every((a) => typeof a === "string") ? { args: tr.args } : {}),
|
|
55
|
-
...(isStringRecord(tr.env) ? { env: tr.env } : {}),
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
else if (tr.kind === "http") {
|
|
59
|
-
if (typeof tr.url !== "string" || !/^https?:\/\//i.test(tr.url))
|
|
60
|
-
return bad("transport.url");
|
|
61
|
-
transport = {
|
|
62
|
-
kind: "http",
|
|
63
|
-
url: tr.url,
|
|
64
|
-
...(isStringRecord(tr.headers) ? { headers: tr.headers } : {}),
|
|
65
|
-
...readMcpIdentityHeaders(tr),
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
return bad("transport.kind");
|
|
70
|
-
}
|
|
71
|
-
const spec = { name: o.name, transport };
|
|
117
|
+
const read = readRequestTransport(o.transport);
|
|
118
|
+
if (!read.ok)
|
|
119
|
+
return bad(read.issue.path);
|
|
120
|
+
const spec = { name: o.name, transport: read.transport };
|
|
72
121
|
if (o.allowTools !== undefined) {
|
|
73
|
-
if (!
|
|
122
|
+
if (!isStringArray(o.allowTools))
|
|
74
123
|
return bad("allowTools");
|
|
75
124
|
spec.allowTools = o.allowTools;
|
|
76
125
|
}
|
|
77
|
-
if (
|
|
126
|
+
if (isElicitationFlag(o.elicitation))
|
|
78
127
|
spec.elicitation = o.elicitation;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
...(typeof a.egress === "boolean" ? { egress: a.egress } : {}),
|
|
88
|
-
...(isToolAxisIrreversibility(a.irreversibility) ? { irreversibility: a.irreversibility } : {}),
|
|
89
|
-
};
|
|
90
|
-
if (Object.keys(entry).length > 0)
|
|
91
|
-
axes[tool] = entry;
|
|
128
|
+
const axes = readMcpToolAxes(o.toolAxes);
|
|
129
|
+
let judged = axes;
|
|
130
|
+
let toolAxesReread;
|
|
131
|
+
if (!axes.ok && storedToolAxes !== undefined) {
|
|
132
|
+
const again = readMcpToolAxes(storedToolAxes(o.toolAxes));
|
|
133
|
+
if (again.ok) {
|
|
134
|
+
judged = again;
|
|
135
|
+
toolAxesReread = axes.problem.replace(CONTROL_AND_BIDI_STRIP_RE, "");
|
|
92
136
|
}
|
|
93
|
-
if (Object.keys(axes).length > 0)
|
|
94
|
-
spec.toolAxes = axes;
|
|
95
137
|
}
|
|
138
|
+
if (!judged.ok)
|
|
139
|
+
return bad("toolAxes");
|
|
140
|
+
if (judged.toolAxes !== undefined)
|
|
141
|
+
spec.toolAxes = judged.toolAxes;
|
|
96
142
|
if (o.contentOrigin !== undefined) {
|
|
97
143
|
if (isContentOriginWord(o.contentOrigin))
|
|
98
144
|
spec.contentOrigin = o.contentOrigin;
|
|
@@ -105,29 +151,32 @@ function normalizeServer(e, noteBadOrigin) {
|
|
|
105
151
|
return bad("toolFaces");
|
|
106
152
|
if (faces.faces !== undefined)
|
|
107
153
|
spec.toolFaces = faces.faces;
|
|
108
|
-
return { ok: true, spec };
|
|
154
|
+
return { ok: true, spec, ...(toolAxesReread !== undefined ? { toolAxesReread } : {}) };
|
|
109
155
|
}
|
|
110
156
|
export function validateRequestMcp(raw) {
|
|
111
|
-
if (raw
|
|
112
|
-
return validateRequestMcpEntries([]);
|
|
113
|
-
if (!Array.isArray(raw))
|
|
157
|
+
if (raw !== undefined && raw !== null && !Array.isArray(raw))
|
|
114
158
|
return null;
|
|
115
|
-
|
|
159
|
+
const { ok, dropped, droppedTotal, badContentOrigin } = validateRequestMcpEntries(Array.isArray(raw) ? raw : []);
|
|
160
|
+
return { ok, dropped, droppedTotal, badContentOrigin };
|
|
116
161
|
}
|
|
117
|
-
function validateRequestMcpEntries(raw) {
|
|
162
|
+
function validateRequestMcpEntries(raw, storedToolAxes) {
|
|
118
163
|
const ok = [];
|
|
119
164
|
const dropped = [];
|
|
120
165
|
const badContentOrigin = [];
|
|
166
|
+
const toolAxesReread = [];
|
|
121
167
|
for (const e of raw.slice(0, MAX_REQUEST_MCP_SERVERS)) {
|
|
122
|
-
const v = normalizeServer(e, (server, value) => badContentOrigin.push({ server, value: describeBadContentOrigin(value) }));
|
|
123
|
-
if (v.ok)
|
|
168
|
+
const v = normalizeServer(e, (server, value) => badContentOrigin.push({ server, value: describeBadContentOrigin(value) }), storedToolAxes);
|
|
169
|
+
if (v.ok) {
|
|
124
170
|
ok.push(v.spec);
|
|
171
|
+
if (v.toolAxesReread !== undefined)
|
|
172
|
+
toolAxesReread.push({ server: v.spec.name, problem: v.toolAxesReread });
|
|
173
|
+
}
|
|
125
174
|
else
|
|
126
175
|
dropped.push({ server: declaredName(e), reason: "malformed_entry", field: v.field });
|
|
127
176
|
}
|
|
128
177
|
for (const e of raw.slice(MAX_REQUEST_MCP_SERVERS, MAX_REQUEST_MCP_SERVERS + MAX_REQUEST_MCP_DROP_NAMES))
|
|
129
178
|
dropped.push({ server: declaredName(e), reason: "over_cap" });
|
|
130
|
-
return { ok, dropped, droppedTotal: Math.max(0, raw.length - ok.length), badContentOrigin };
|
|
179
|
+
return { ok, dropped, droppedTotal: Math.max(0, raw.length - ok.length), badContentOrigin, toolAxesReread };
|
|
131
180
|
}
|
|
132
181
|
export function assertRequestMcpUnlocked(bodyMcp, lockedKeys) {
|
|
133
182
|
if (!lockedKeys.has("mcp"))
|
|
@@ -138,30 +187,6 @@ export function assertRequestMcpUnlocked(bodyMcp, lockedKeys) {
|
|
|
138
187
|
code: "config.locked_key",
|
|
139
188
|
});
|
|
140
189
|
}
|
|
141
|
-
export function assertRequestMcpContentOrigin(bodyMcp) {
|
|
142
|
-
if (!Array.isArray(bodyMcp))
|
|
143
|
-
return;
|
|
144
|
-
for (const e of bodyMcp) {
|
|
145
|
-
if (!e || typeof e !== "object" || Array.isArray(e))
|
|
146
|
-
continue;
|
|
147
|
-
const v = e.contentOrigin;
|
|
148
|
-
if (v === undefined)
|
|
149
|
-
continue;
|
|
150
|
-
if (isContentOriginWord(v))
|
|
151
|
-
continue;
|
|
152
|
-
throw new HttpError(400, `mcpServers[].contentOrigin must be one of ${CONTENT_ORIGIN_WORDS.map((w) => `"${w}"`).join(" | ")} — got ${describeBadContentOrigin(v)}` +
|
|
153
|
-
` (omit the key entirely for the default: this server's tools classify as external content)`, { code: "request.field_invalid" });
|
|
154
|
-
}
|
|
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
190
|
export function requestEntryIdentityIssue(entry) {
|
|
166
191
|
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
167
192
|
return undefined;
|
|
@@ -170,15 +195,81 @@ export function requestEntryIdentityIssue(entry) {
|
|
|
170
195
|
return undefined;
|
|
171
196
|
return mcpIdentityHeadersIssue(t);
|
|
172
197
|
}
|
|
173
|
-
export function requestEntryIdentityRefusal(entry) {
|
|
198
|
+
export function requestEntryIdentityRefusal(entry, at) {
|
|
174
199
|
const issue = requestEntryIdentityIssue(entry);
|
|
175
|
-
if (issue === undefined)
|
|
200
|
+
if (issue === undefined || !isRecord(entry))
|
|
176
201
|
return undefined;
|
|
177
|
-
const name = echoKeyName("", declaredName(entry));
|
|
178
202
|
if (issue === "both_set")
|
|
179
|
-
return { errorCode: "request.body_shape", message: describeMcpIdentityHeadersConflict(
|
|
180
|
-
const
|
|
181
|
-
|
|
203
|
+
return { errorCode: "request.body_shape", message: describeMcpIdentityHeadersConflict(echoKeyName("", declaredName(entry))) };
|
|
204
|
+
const transport = entry.transport;
|
|
205
|
+
const value = isRecord(transport) ? transport.assertionHeader : undefined;
|
|
206
|
+
return entryFieldRefusal(entry, at, { path: "transport.assertionHeader", reason: describeMcpAssertionHeaderNotAString(value) });
|
|
207
|
+
}
|
|
208
|
+
export function assertRequestMcpEntries(bodyMcp) {
|
|
209
|
+
if (!Array.isArray(bodyMcp))
|
|
210
|
+
return;
|
|
211
|
+
for (let i = 0; i < Math.min(bodyMcp.length, MAX_REQUEST_MCP_SERVERS); i++) {
|
|
212
|
+
const refusal = requestEntryRefusal(bodyMcp[i], `mcpServers[${i}]`);
|
|
213
|
+
if (refusal !== undefined)
|
|
214
|
+
throw createCodedHttpError(400, refusal.errorCode, refusal.message);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const ENTRY_KEY_JUDGES = {
|
|
218
|
+
allowTools: (o) => o.allowTools === undefined || isStringArray(o.allowTools)
|
|
219
|
+
? undefined
|
|
220
|
+
: { path: "allowTools", reason: `must be an array of strings (omit the key to leave the server's tools unrestricted) — got ${describeNotStringArray(o.allowTools)}` },
|
|
221
|
+
contentOrigin: (o) => o.contentOrigin === undefined || isContentOriginWord(o.contentOrigin)
|
|
222
|
+
? undefined
|
|
223
|
+
: {
|
|
224
|
+
path: "contentOrigin",
|
|
225
|
+
reason: `must be one of ${CONTENT_ORIGIN_WORDS.map((w) => `"${w}"`).join(" | ")} — got ${describeBadContentOrigin(o.contentOrigin)}` +
|
|
226
|
+
` (omit the key for the default: this server's tools classify as external content)`,
|
|
227
|
+
},
|
|
228
|
+
elicitation: (o) => (o.elicitation === undefined || isElicitationFlag(o.elicitation) ? undefined : { path: "elicitation", reason: `must be a boolean — got ${describeShape(o.elicitation)}` }),
|
|
229
|
+
name: (o) => (isUsableWireName(o.name) ? undefined : { path: "name", reason: `must be a non-empty string within the wire-name length bound — got ${describeBadWireName(o.name)}` }),
|
|
230
|
+
source: (o) => o.source === undefined || isMcpSourceLabel(o.source)
|
|
231
|
+
? undefined
|
|
232
|
+
: { path: "source", reason: `must be a non-empty string of at most ${MCP_SOURCE_LABEL_MAX} characters (a display label) — got ${describeShape(o.source)}` },
|
|
233
|
+
toolAxes: (o) => {
|
|
234
|
+
const read = readMcpToolAxes(o.toolAxes);
|
|
235
|
+
return read.ok ? undefined : { path: "toolAxes", reason: read.problem };
|
|
236
|
+
},
|
|
237
|
+
toolFaces: (o) => {
|
|
238
|
+
const read = readMcpToolFaces(o.toolFaces);
|
|
239
|
+
return read.ok ? undefined : { path: "toolFaces", reason: read.problem };
|
|
240
|
+
},
|
|
241
|
+
transport: (o) => {
|
|
242
|
+
const read = readRequestTransport(o.transport);
|
|
243
|
+
return read.ok ? read.lost[0] : read.issue;
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
const ENTRY_REFUSAL_TAIL = "The request is refused rather than mounting the server without it: a declaration that silently did not apply would leave the server running differently from what you declared. " +
|
|
247
|
+
"Fix the entry (an optional key may also be omitted).";
|
|
248
|
+
function entryFieldRefusal(entry, at, issue) {
|
|
249
|
+
const server = echoKeyName("", declaredName(entry));
|
|
250
|
+
return {
|
|
251
|
+
errorCode: "request.field_invalid",
|
|
252
|
+
message: `${at}.${issue.path} (server "${server}") is not a usable declaration — ${issue.reason.replace(CONTROL_AND_BIDI_STRIP_RE, "")}. ${ENTRY_REFUSAL_TAIL}`,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function requestEntryRefusal(entry, at) {
|
|
256
|
+
if (!isRecord(entry))
|
|
257
|
+
return undefined;
|
|
258
|
+
const identity = requestEntryIdentityRefusal(entry, at);
|
|
259
|
+
if (identity !== undefined)
|
|
260
|
+
return identity;
|
|
261
|
+
for (const key of MCP_SERVER_ENTRY_KEYS) {
|
|
262
|
+
const issue = ENTRY_KEY_JUDGES[key](entry);
|
|
263
|
+
if (issue !== undefined)
|
|
264
|
+
return entryFieldRefusal(entry, at, issue);
|
|
265
|
+
}
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
export function requestEntryToolAxesRefusal(entry, at) {
|
|
269
|
+
if (!isRecord(entry))
|
|
270
|
+
return undefined;
|
|
271
|
+
const issue = ENTRY_KEY_JUDGES.toolAxes(entry);
|
|
272
|
+
return issue === undefined ? undefined : entryFieldRefusal(entry, at, issue);
|
|
182
273
|
}
|
|
183
274
|
export function deploymentClosedMcpNames(baseline, reservedNames) {
|
|
184
275
|
return new Set([...(baseline ?? []).map((s) => s.name), ...reservedNames]);
|
|
@@ -190,20 +281,20 @@ export function mergeRequestMcp(baseline, gated, reservedNames) {
|
|
|
190
281
|
const add = gated.filter((s) => !closed.has(s.name));
|
|
191
282
|
return add.length === 0 ? baseline : [...(baseline ?? []), ...add];
|
|
192
283
|
}
|
|
193
|
-
export function resolveRequestMcp(baseline, bodyMcp, config, logger, notice) {
|
|
284
|
+
export function resolveRequestMcp(baseline, bodyMcp, config, logger, notice, storedToolAxes) {
|
|
194
285
|
if (bodyMcp === undefined || bodyMcp === null)
|
|
195
286
|
return baseline;
|
|
196
|
-
const { mcp, drops } = classifyRequestMcp(baseline, bodyMcp, config, logger);
|
|
287
|
+
const { mcp, drops } = classifyRequestMcp(baseline, bodyMcp, config, logger, storedToolAxes);
|
|
197
288
|
mintInjectionDropNotices(drops, notice);
|
|
198
289
|
return mcp;
|
|
199
290
|
}
|
|
200
|
-
function classifyRequestMcp(baseline, bodyMcp, config, logger) {
|
|
291
|
+
function classifyRequestMcp(baseline, bodyMcp, config, logger, storedToolAxes) {
|
|
201
292
|
if (bodyMcp !== undefined && bodyMcp !== null && !Array.isArray(bodyMcp)) {
|
|
202
293
|
logger?.warn("mcp_injection_ignored", { reason: "mcpServers not an array" });
|
|
203
294
|
return { mcp: baseline, drops: [] };
|
|
204
295
|
}
|
|
205
296
|
const entries = Array.isArray(bodyMcp) ? bodyMcp : [];
|
|
206
|
-
const outcome = mcpInjectionHonored(config) ? classifyHonored(baseline, entries, config, logger) : classifyGateClosed(baseline, entries);
|
|
297
|
+
const outcome = mcpInjectionHonored(config) ? classifyHonored(baseline, entries, config, logger, storedToolAxes) : classifyGateClosed(baseline, entries);
|
|
207
298
|
if (outcome.drops.length > 0)
|
|
208
299
|
logger?.warn("mcp_injection_dropped", { dropped: outcome.drops, droppedTotal: outcome.droppedTotal });
|
|
209
300
|
return { mcp: outcome.mcp, drops: outcome.drops };
|
|
@@ -215,8 +306,8 @@ function classifyGateClosed(baseline, entries) {
|
|
|
215
306
|
droppedTotal: entries.length,
|
|
216
307
|
};
|
|
217
308
|
}
|
|
218
|
-
function classifyHonored(baseline, entries, config, logger) {
|
|
219
|
-
const v = validateRequestMcpEntries(entries);
|
|
309
|
+
function classifyHonored(baseline, entries, config, logger, storedToolAxes) {
|
|
310
|
+
const v = validateRequestMcpEntries(entries, storedToolAxes);
|
|
220
311
|
if (v.badContentOrigin.length > 0) {
|
|
221
312
|
logger?.warn("mcp_content_class_dropped", {
|
|
222
313
|
entries: v.badContentOrigin,
|
|
@@ -236,6 +327,16 @@ function classifyHonored(baseline, entries, config, logger) {
|
|
|
236
327
|
if (issue !== undefined && mounted.has(declaredName(e)))
|
|
237
328
|
noteMcpAssertionHeaderIgnored(logger, declaredName(e), issue);
|
|
238
329
|
}
|
|
330
|
+
for (const r of v.toolAxesReread) {
|
|
331
|
+
if (!mounted.has(r.server))
|
|
332
|
+
continue;
|
|
333
|
+
logger?.warn("mcp_tool_axes_read_as_before_upgrade", {
|
|
334
|
+
server: r.server,
|
|
335
|
+
migration: "M19",
|
|
336
|
+
problem: r.problem,
|
|
337
|
+
note: "this replayed request was stored before the upgrade and its toolAxes declaration does not pass the engine's reader; while one-time migration M19 is open it is read the way the pre-upgrade server read it (members that did not fit are dropped, a declaration that is not an object counts as none) and the server stays mounted — once the window closes the same request leaves this server unmounted",
|
|
338
|
+
});
|
|
339
|
+
}
|
|
239
340
|
return {
|
|
240
341
|
mcp: mergeRequestMcp(baseline, v.ok.map(toCoreMcpServerSpec), reservedNames),
|
|
241
342
|
drops: [...v.dropped, ...blocked.map((server) => ({ server, reason: "name_reserved_by_deployment" }))],
|
package/dist/task-settings.d.ts
CHANGED
|
@@ -47,9 +47,10 @@
|
|
|
47
47
|
* - (`hooks` NO LONGER deferred either — 轴B #2 注释保鲜:applied 形已接线(本文件 hooks 分片 +
|
|
48
48
|
* main.ts 单用户闸),仅 malformed 才报 deferred;旧「需要 remote hook-runner」是接线前拍照。)
|
|
49
49
|
*/
|
|
50
|
-
import { type ExecutionEnv, type TaskSpec, type ThinkingLevel, type ToolPolicy, type PromptProvider, type PermissionMode } from "@sema-agent/core";
|
|
51
|
-
import type { HooksConfig } from "@sema-agent/settings-schema/hooks";
|
|
50
|
+
import { type PathRuleBases, type ExecutionEnv, type TaskSpec, type ThinkingLevel, type ToolPolicy, type PromptProvider, type PermissionMode } from "@sema-agent/core";
|
|
52
51
|
import { type KeyClosureIssue } from "./request-key-closure.js";
|
|
52
|
+
import { type RunnableHooksConfig } from "./hooks/hook-runner.js";
|
|
53
|
+
import type { StoredHooksRereadM20 } from "./stored-request-hooks-m20.js";
|
|
53
54
|
import { type EntryPermissionMode } from "./runtime-governance.js";
|
|
54
55
|
/**
|
|
55
56
|
* 请求面 `permissionMode` 的闭集 = core `PERMISSION_MODES` **全集**(顶层 `permissionMode` / `settings.permissions.defaultMode` /
|
|
@@ -213,10 +214,10 @@ export interface ParsedTaskSettings {
|
|
|
213
214
|
* orthogonal axes — `thinking:"max"` (the reasoning axis) + `selfOrchestration:true` (the orchestration axis,
|
|
214
215
|
* through its existing fail-closed gate). NOT a new mechanism; just a preset that fans out to existing spec fields. */
|
|
215
216
|
ultracode?: boolean;
|
|
216
|
-
/** hooks hook-runner(阶段一)
|
|
217
|
+
/** hooks hook-runner(阶段一):经 `parseHooksConfig`(契约包分类器 + 服务侧上限 + 本仓会跑的条目类型)判过的 hooks 配置。像 shellEnv 一样
|
|
217
218
|
* 这里只 PARSE;应用点(main.ts resolveSpec)按单用户闸(`requirePrincipal !== true`)决定翻不翻成
|
|
218
219
|
* core `TaskSpec.hooks` 回调——多租户 lane 收到只警告忽略(hook 命令跑在 worker host = class ② 能力授予)。 */
|
|
219
|
-
hooks?:
|
|
220
|
+
hooks?: RunnableHooksConfig;
|
|
220
221
|
/** [ref](7.77.0)—— CC settings 同名键:上下文窗口里**留给技能清单**的比例((0,1];CC 默认 0.01 = 1%)。
|
|
221
222
|
* 超预算时引擎把描述裁短、再不够退成 name-only(`cli259.js` 的清单渲染算法),**从不**拒掉一条技能 ——
|
|
222
223
|
* 这正是本版把技能条数/描述两条帽整条退役后的补偿面:per-turn 成本的旋钮在**渲染侧**,不在受理侧。
|
|
@@ -232,6 +233,15 @@ export interface ParseTaskSettingsResult {
|
|
|
232
233
|
/** Sub-fields the caller SENT but this version does not wire(`env`;`hooks` 仅在 MALFORMED 时入列——valid hooks
|
|
233
234
|
* 已是 applied 字段)。Empty when none. */
|
|
234
235
|
deferred: string[];
|
|
236
|
+
/** 一次性迁移 M20(窗口见 `plugins/one-time-migrations.ts` 登记表):存下的 hooks 被唯一读者拒掉、经重读座剥掉点名的键后过了读者 ⇒ 本次读数里 `settings.hooks` 是重读的产物,
|
|
237
|
+
* 并带被剥的坐标(出门形)。缺席 = 没有发生重读。 */
|
|
238
|
+
hooksReread?: {
|
|
239
|
+
readonly stripped: readonly string[];
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
/** `parseTaskSettings` 的可选座:续跑重放腿才有的重读座(一次性迁移 M20;新提交腿与不问 hooks 的读者不递)。 */
|
|
243
|
+
export interface ParseTaskSettingsSeats {
|
|
244
|
+
readonly storedHooksReread?: StoredHooksRereadM20;
|
|
235
245
|
}
|
|
236
246
|
/** Cap on `outputStyle` length — it lands in the system prompt, so it shares the `MAX_SYSTEM_PROMPT_CHARS` (16384)
|
|
237
247
|
* posture (an uncapped prompt body is a per-turn token-cost hole; cross review). Exported so the HTTP layer
|
|
@@ -333,7 +343,7 @@ export declare function taskHooksHonored(config: {
|
|
|
333
343
|
* 重放也走的宽容层,一个 4xx 在这里会把存量 parked 任务永久砖死;FRESH 提交面由 {@link taskSettingsKeyIssue}
|
|
334
344
|
* 在 `server.ts` prepareSpec 里 400 响亮拒 + 逐字列名。双层姿势与 outputStyle / permissionMode 两处逐字相同。
|
|
335
345
|
*/
|
|
336
|
-
export declare function parseTaskSettings(raw: unknown): ParseTaskSettingsResult;
|
|
346
|
+
export declare function parseTaskSettings(raw: unknown, seats?: ParseTaskSettingsSeats): ParseTaskSettingsResult;
|
|
337
347
|
/** Defensive acceptance of the `appendSystemPrompt` rider ([ref] R2 + codex F2): the HTTP 400 gate (prepareSpec,
|
|
338
348
|
* MAX_SYSTEM_PROMPT_CHARS) only covers FRESH submits — every resume family re-enters resolveSpec on a STORED body,
|
|
339
349
|
* and a pre-1.243 store preserved unknown request keys verbatim (the field was INERT then), so an over-cap legacy
|
|
@@ -457,6 +467,32 @@ export interface FsWriteGateWiring {
|
|
|
457
467
|
* 本 wiring 的组合点。) */
|
|
458
468
|
isExempt?: (toolName: string, canonicalPath: string) => boolean | Promise<boolean>;
|
|
459
469
|
}
|
|
470
|
+
/**
|
|
471
|
+
* deny / ask 两桶(core 规则编译器读的收紧表)的**问序,单表**:{@link rulePolicyBucketsRefusal} 的两段循环(名字层 + 文法 · `~/` 要 home)都迭代它 ——
|
|
472
|
+
* 两桶都有坏条目时,拒点与存体预判点名同一个桶、说同一句。表里每个词同时是 core 规则的 `behavior`。
|
|
473
|
+
*/
|
|
474
|
+
export declare const RULE_POLICY_BUCKETS: readonly ["ask", "deny"];
|
|
475
|
+
export type RulePolicyBucket = (typeof RULE_POLICY_BUCKETS)[number];
|
|
476
|
+
/**
|
|
477
|
+
* deny / ask 两桶的**完整规则判**,唯一一只(拒点 {@link rejectUnmatchableSettingsNames} 与存体预判 `http/stored-body-replay.ts` 同调):
|
|
478
|
+
* ② 按 {@link RULE_POLICY_BUCKETS} 的序逐桶问 `unmatchableToolNamesRefusal`(名字层 + core 规则文法);③ 都过了,再逐桶判 `~/` 形要 home 的那一拒。第一个坏处答 `{ bucket, message }`。
|
|
479
|
+
* 入参:桶、{@link RulePolicyBases}。**home 同源**:拒点的 home 是 `resolve-spec.ts` 的 `laneHomeBase()` 经 `executionLaneHomeDir(config)` 算出的,预判传同一个函数对同一份部署配置算出的。
|
|
480
|
+
* **声明给文法的基**只有拒点有(根 + home 的并集,执行车道才知道根):core 的规则文法对**没声明**的基不下判词(见 `config.ts` `uncompilableRuleGuidance`),所以预判什么也不声明、两腿逐字同句;
|
|
481
|
+
* 预判不能把 home 当「半个基」声明给文法 —— core 的路径族只从根推,缺根时一个合法的 Windows 形 home 会按默认族被判 `unsupported.path_base`(V6-k)。
|
|
482
|
+
* ③ 排在最后:一条同时文法不成立又带 `~/` 的规则,先说文法(补上 home 它仍被文法拒,先说 home 是误导)。
|
|
483
|
+
*/
|
|
484
|
+
export interface RulePolicyBases {
|
|
485
|
+
/** 声明给 core 规则文法的路径基(这条 task 的根 + home):拒点才有;存体预判不声明。 */
|
|
486
|
+
readonly declared?: PathRuleBases;
|
|
487
|
+
/** 执行环境的 home(`~/` 形规则的前提);这个部署解不出 ⇒ 缺席。 */
|
|
488
|
+
readonly home?: string;
|
|
489
|
+
}
|
|
490
|
+
export declare function rulePolicyBucketsRefusal(permissions: Readonly<Partial<Record<RulePolicyBucket, readonly string[]>>>, bases: RulePolicyBases): {
|
|
491
|
+
readonly bucket: RulePolicyBucket;
|
|
492
|
+
readonly message: string;
|
|
493
|
+
} | undefined;
|
|
494
|
+
/** 请求的 settings 编不成生效的规则 / 有永远不生效的条目 ⇒ 422 的句首(三只桶同一个出口;`resolveSpec` 的拒点与存体重放预判的 409 句子同用这一个,不各写一份)。 */
|
|
495
|
+
export declare const REQUEST_SETTINGS_REFUSAL_PREFIX = "request settings cannot be applied as written: ";
|
|
460
496
|
export declare function deriveSettingsPolicy(settings: ParsedTaskSettings, gate?: FsWriteGateWiring, workflowGate?: WorkflowGateWiring): {
|
|
461
497
|
toolPolicy?: ToolPolicy;
|
|
462
498
|
};
|