@sema-agent/server 7.130.0 → 7.132.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 +396 -0
- package/README.md +13 -6
- package/README.zh-CN.md +9 -5
- package/USAGE.md +187 -50
- package/dist/approval-card.d.ts +3 -1
- package/dist/approval-card.js +2 -3
- package/dist/audit.d.ts +7 -10
- package/dist/audit.js +4 -1
- package/dist/auth-bridge.d.ts +4 -0
- package/dist/auth-bridge.js +10 -11
- package/dist/bench/s1/live-deps.js +2 -0
- package/dist/boot/deployment-mods.d.ts +31 -0
- package/dist/boot/deployment-mods.js +26 -0
- package/dist/boot/execution-env.d.ts +1 -1
- package/dist/boot/execution-env.js +19 -6
- package/dist/boot/memory-boundary.d.ts +2 -2
- package/dist/boot/read-deny-anchor-audit.d.ts +17 -0
- package/dist/boot/read-deny-anchor-audit.js +36 -0
- package/dist/boot/resolve-spec.d.ts +1 -5
- package/dist/boot/resolve-spec.js +56 -61
- package/dist/boot/runner-deps.d.ts +19 -8
- package/dist/boot/runner-deps.js +14 -2
- package/dist/boot/session-faces.d.ts +1 -7
- package/dist/boot/stage-02-web-admin-pull.js +9 -1
- package/dist/boot/stage-03-store-backend.d.ts +1 -1
- package/dist/boot/stage-03-store-backend.js +2 -2
- package/dist/boot/stage-04-stores.d.ts +1 -1
- package/dist/boot/stage-05-execution-env.d.ts +4 -4
- package/dist/boot/stage-05-execution-env.js +7 -10
- package/dist/boot/stage-06-runners.d.ts +2 -2
- package/dist/boot/stage-06-runners.js +5 -4
- package/dist/boot/stage-07-capability-layer.d.ts +2 -2
- package/dist/boot/stage-07-capability-layer.js +3 -3
- package/dist/boot/stage-08-reapers.d.ts +2 -2
- package/dist/boot/stage-09-leader.d.ts +3 -9
- package/dist/boot/stage-10-http-server.d.ts +3 -9
- package/dist/boot/stage-10-http-server.js +12 -10
- package/dist/boot/stores.d.ts +1 -1
- package/dist/boot/stores.js +5 -6
- package/dist/boot/web-admin.d.ts +0 -3
- package/dist/boot/web-admin.js +35 -35
- package/dist/brain.js +17 -22
- package/dist/budget.js +5 -4
- package/dist/caller-text-echo.d.ts +20 -0
- package/dist/caller-text-echo.js +24 -0
- package/dist/capabilities/brand-identity.d.ts +26 -0
- package/dist/capabilities/brand-identity.js +27 -0
- package/dist/capabilities/builtin-tools.js +2 -1
- package/dist/capabilities/center-plugins.js +1 -1
- package/dist/capabilities/center-prompts.d.ts +32 -59
- package/dist/capabilities/center-prompts.js +18 -32
- package/dist/capabilities/composite-members.d.ts +1 -0
- package/dist/capabilities/composite-members.js +1 -0
- package/dist/capabilities/engine-capabilities.js +2 -1
- package/dist/capabilities/hands-lane.d.ts +9 -6
- package/dist/capabilities/hands-lane.js +3 -3
- package/dist/capabilities/host-tool-names.d.ts +17 -0
- package/dist/capabilities/host-tool-names.js +15 -0
- package/dist/capabilities/mcp-probe.d.ts +14 -1
- package/dist/capabilities/mcp-probe.js +22 -133
- package/dist/capabilities/repo-tools.js +4 -3
- package/dist/capabilities/scenarios.d.ts +42 -47
- package/dist/capabilities/scenarios.js +19 -35
- 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.d.ts +19 -4
- package/dist/capabilities/skills.js +30 -10
- package/dist/capabilities/tool-defer.js +2 -1
- package/dist/config-catalog.js +22 -11
- package/dist/config-invariants.js +2 -2
- package/dist/config-lkg.d.ts +9 -9
- package/dist/config-lkg.js +20 -4
- package/dist/config-provider.d.ts +3 -1
- package/dist/config-provider.js +14 -6
- package/dist/config-types.d.ts +13 -9
- package/dist/config.d.ts +2 -2
- package/dist/config.js +34 -20
- package/dist/device-ws-protocol.d.ts +1 -1
- package/dist/device-ws-protocol.js +1 -0
- package/dist/elicitation.d.ts +7 -0
- package/dist/elicitation.js +20 -5
- package/dist/env-name-allowlist-knobs.d.ts +12 -2
- package/dist/env-name-allowlist-knobs.js +5 -4
- package/dist/exec-bounds.d.ts +15 -3
- package/dist/exec-bounds.js +4 -0
- package/dist/execution-lane-caps.d.ts +9 -2
- package/dist/execution-lane-caps.js +11 -0
- package/dist/hooks/classic-stations.d.ts +86 -0
- package/dist/hooks/classic-stations.js +27 -0
- package/dist/hooks/hook-llm.d.ts +8 -5
- package/dist/hooks/hook-llm.js +80 -37
- package/dist/hooks/hook-runner.d.ts +273 -75
- package/dist/hooks/hook-runner.js +293 -317
- package/dist/http/admission.d.ts +8 -2
- package/dist/http/admission.js +103 -59
- package/dist/http/approval-sweeps.js +59 -18
- package/dist/http/checkpoint-ctx.d.ts +36 -18
- package/dist/http/checkpoint-ctx.js +14 -10
- package/dist/http/claimed-run.d.ts +13 -0
- package/dist/http/claimed-run.js +15 -0
- package/dist/http/device-placement.js +5 -10
- package/dist/http/dispatch.d.ts +0 -4
- package/dist/http/dispatch.js +6 -10
- package/dist/http/durable-run-leg.d.ts +2 -1
- package/dist/http/durable-run-leg.js +5 -3
- package/dist/http/health-self-report.d.ts +7 -5
- package/dist/http/health-self-report.js +4 -18
- package/dist/http/principal-gate.d.ts +23 -6
- package/dist/http/principal-gate.js +24 -5
- package/dist/http/refusal-body.d.ts +21 -0
- package/dist/http/refusal-body.js +4 -0
- package/dist/http/request-guards.d.ts +27 -0
- package/dist/http/request-guards.js +24 -3
- package/dist/http/resume-legs.d.ts +25 -0
- package/dist/http/resume-legs.js +48 -26
- package/dist/http/routes/approvals-assistant.js +1 -1
- package/dist/http/routes/capabilities.js +7 -17
- package/dist/http/routes/diagnostics.js +1 -1
- package/dist/http/routes/rules.js +2 -2
- package/dist/http/routes/runs.js +13 -17
- package/dist/http/routes/session-sync.js +4 -2
- package/dist/http/routes/sessions.js +23 -11
- package/dist/http/routes/shared-memory.js +2 -13
- package/dist/http/routes/side-query.js +2 -3
- package/dist/http/routes/tasks.js +34 -19
- 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 +13 -1
- package/dist/http/send.js +12 -1
- package/dist/http/server.d.ts +37 -23
- package/dist/http/server.js +30 -4
- package/dist/http/stored-body-replay.d.ts +10 -3
- package/dist/http/stored-body-replay.js +27 -6
- package/dist/http/submit-ctx.d.ts +25 -0
- package/dist/http/submit-ctx.js +39 -0
- package/dist/http/wire-types.d.ts +18 -5
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/lane-home-probe.d.ts +43 -0
- package/dist/lane-home-probe.js +35 -0
- package/dist/limit-refusal.d.ts +47 -0
- package/dist/limit-refusal.js +12 -0
- package/dist/main.js +1 -1
- package/dist/mcp-assertion-header.d.ts +3 -11
- package/dist/mcp-assertion-header.js +3 -9
- package/dist/mcp-tool-faces.d.ts +5 -0
- package/dist/mcp-tool-faces.js +5 -1
- package/dist/memory-scope.d.ts +1 -1
- package/dist/model-entry-refusal.d.ts +1 -7
- package/dist/model-provider.d.ts +39 -12
- package/dist/model-provider.js +19 -2
- package/dist/model-route-endpoint.d.ts +52 -12
- package/dist/model-route-endpoint.js +48 -26
- 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/cost-taxonomy.d.ts +7 -7
- package/dist/observability/fail-open.d.ts +53 -12
- package/dist/observability/fail-open.js +47 -7
- package/dist/observability/metrics.js +3 -1
- package/dist/observability/prompt-manifest.d.ts +47 -82
- package/dist/observability/prompt-manifest.js +119 -17
- package/dist/observability/run-terminal.js +14 -6
- package/dist/observability/secret-env-scrub.d.ts +13 -1
- package/dist/observability/secret-env-scrub.js +13 -1
- package/dist/observability/tool-trace.d.ts +16 -21
- package/dist/observability/tool-trace.js +51 -42
- package/dist/per-task-image.d.ts +7 -1
- package/dist/per-task-image.js +3 -3
- package/dist/plain-string-record.d.ts +25 -0
- package/dist/plain-string-record.js +32 -0
- package/dist/plugins/blob-backend.d.ts +68 -13
- package/dist/plugins/blob-backend.js +53 -10
- package/dist/plugins/checkpoint-store-sql.d.ts +14 -4
- package/dist/plugins/checkpoint-store-sql.js +39 -26
- package/dist/plugins/k8s-bg-scripts.d.ts +11 -0
- package/dist/plugins/k8s-bg-scripts.js +22 -0
- package/dist/plugins/local-checkpoint-store.js +5 -2
- package/dist/plugins/local-session-store.js +3 -3
- package/dist/plugins/one-time-migrations.d.ts +147 -91
- package/dist/plugins/one-time-migrations.js +372 -208
- package/dist/plugins/permission-rule-store-sql.d.ts +13 -7
- package/dist/plugins/permission-rule-store-sql.js +4 -18
- package/dist/plugins/pg-session-storage.d.ts +4 -5
- package/dist/plugins/pg-session-storage.js +7 -17
- package/dist/plugins/remote-env-adb.d.ts +6 -0
- package/dist/plugins/remote-env-adb.js +9 -4
- package/dist/plugins/remote-env-device.js +3 -0
- package/dist/plugins/remote-env-e2b.d.ts +1 -1
- package/dist/plugins/remote-env-e2b.js +21 -9
- package/dist/plugins/remote-env-host.d.ts +9 -9
- package/dist/plugins/remote-env-host.js +10 -32
- package/dist/plugins/remote-env-k8s.d.ts +95 -26
- package/dist/plugins/remote-env-k8s.js +265 -90
- package/dist/plugins/remote-env-local-docker.d.ts +20 -1
- package/dist/plugins/remote-env-local-docker.js +77 -9
- package/dist/plugins/remote-env-ssh.d.ts +15 -0
- package/dist/plugins/remote-env-ssh.js +109 -3
- package/dist/plugins/remote-shell.d.ts +0 -1
- package/dist/plugins/remote-shell.js +1 -2
- package/dist/plugins/retention-store-sql.js +6 -2
- package/dist/plugins/schema-consistency.d.ts +54 -0
- package/dist/plugins/schema-consistency.js +52 -0
- package/dist/plugins/session-epoch-anchor.d.ts +13 -0
- package/dist/plugins/session-epoch-anchor.js +23 -0
- package/dist/plugins/shared-memory-store-sql.d.ts +9 -3
- package/dist/plugins/shared-memory-store-sql.js +5 -16
- package/dist/plugins/sql-driver.d.ts +141 -0
- package/dist/plugins/sql-driver.js +115 -1
- package/dist/plugins/sql-errors.d.ts +41 -0
- package/dist/plugins/sql-errors.js +36 -0
- package/dist/plugins/store-backend.js +2 -1
- package/dist/plugins/tidb-session-storage.d.ts +7 -11
- package/dist/plugins/tidb-session-storage.js +6 -16
- package/dist/plugins/tidb-session-store.js +1 -1
- package/dist/prompts-domain-validate.d.ts +17 -61
- package/dist/prompts-domain-validate.js +9 -119
- package/dist/question.d.ts +18 -9
- package/dist/question.js +21 -13
- package/dist/request-key-closure.d.ts +4 -4
- package/dist/request-key-closure.js +18 -13
- package/dist/rule-entry-list.d.ts +134 -24
- package/dist/rule-entry-list.js +162 -30
- package/dist/run-local.d.ts +3 -3
- package/dist/run-local.js +26 -25
- package/dist/runs.d.ts +48 -2
- package/dist/runs.js +61 -2
- package/dist/server-secret-env.d.ts +44 -18
- package/dist/server-secret-env.js +49 -8
- package/dist/session-incarnation.d.ts +15 -5
- package/dist/session-incarnation.js +16 -9
- package/dist/session-titler.js +12 -2
- package/dist/spec-fields.d.ts +34 -18
- package/dist/spec-fields.js +46 -53
- package/dist/sql-engine-posture.d.ts +29 -4
- package/dist/sql-engine-posture.js +36 -1
- 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 +20 -5
- package/dist/stored-request-tool-axes-m19.js +30 -2
- package/dist/task-a2a.d.ts +9 -1
- package/dist/task-a2a.js +73 -47
- package/dist/task-mcp.d.ts +54 -102
- package/dist/task-mcp.js +194 -92
- package/dist/task-settings.d.ts +47 -25
- package/dist/task-settings.js +46 -61
- package/dist/text-bidi.d.ts +3 -46
- package/dist/text-bidi.js +3 -3
- package/dist/tool-approval.d.ts +1 -1
- package/dist/trace/core-keyset-guard.d.ts +24 -4
- package/dist/trace/engine-notice-wire.d.ts +11 -3
- package/dist/trace/engine-notice-wire.js +8 -1
- package/dist/trace/ledger-events.d.ts +3 -1
- package/dist/trace/ledger-sink.d.ts +29 -0
- package/dist/trace/ledger-sink.js +15 -1
- package/dist/trace/model-segment.js +1 -1
- package/dist/trace/project.d.ts +25 -6
- package/dist/trace/project.js +68 -22
- package/dist/trace/wire-projection-faces.d.ts +80 -2
- package/dist/trace/wire-projection-faces.js +23 -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/candidate-gate.d.ts +6 -6
- package/dist/web-admin-client/candidate-gate.js +3 -3
- package/dist/web-admin-client/http-client.js +0 -6
- package/dist/web-admin-client/read-warnings.d.ts +13 -0
- package/dist/web-admin-client/read-warnings.js +10 -0
- package/dist/web-admin-client/skills-mcp.d.ts +3 -0
- package/dist/web-admin-client/skills-mcp.js +9 -6
- package/dist/web-admin-client/types.d.ts +3 -4
- package/dist/workspace-cache.d.ts +35 -0
- package/dist/workspace-cache.js +36 -0
- package/dist/workspace-space.d.ts +3 -2
- package/dist/workspace-space.js +2 -0
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +8 -1
- package/package.json +4 -4
- package/scripts/drain-check-local.mjs +37 -3
- package/scripts/drain-check.mysql.sql +30 -2
- package/scripts/drain-check.pg.sql +29 -2
- package/dist/capabilities/memory-notice.d.ts +0 -138
- package/dist/capabilities/memory-notice.js +0 -64
- package/dist/capabilities/prompt.d.ts +0 -17
- package/dist/capabilities/prompt.js +0 -6
- package/dist/mcp-content-origin.d.ts +0 -34
- package/dist/mcp-content-origin.js +0 -10
- package/dist/tool-axis-words.d.ts +0 -27
- package/dist/tool-axis-words.js +0 -9
- package/dist/wire-name-bound.d.ts +0 -14
- package/dist/wire-name-bound.js +0 -13
package/dist/task-mcp.d.ts
CHANGED
|
@@ -37,10 +37,9 @@ export declare const MAX_REQUEST_MCP_SERVERS = 32;
|
|
|
37
37
|
* ——同一条链上后续的正常事件全部排在它们后面。修前的名字数组把超上限折成**一条**聚合串,所以无界的
|
|
38
38
|
* 是本批新写的那两个循环。
|
|
39
39
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* body
|
|
43
|
-
* 有界点名两头都占:该说的仍然说得出,量级钉死在与挂载同阶。
|
|
40
|
+
* 新提交腿上超过 {@link MAX_REQUEST_MCP_SERVERS} 条的列表在腿首整份 400({@link requestMcpDeclarationRefusal}),走不到这里;
|
|
41
|
+
* 有界点名守的是**重放存量 body** 的腿(那条腿不拒 —— 砖死 resume 比降级糟,本仓 `opts.leg` 三处先例)与闸关腿:
|
|
42
|
+
* 存量 body 的长度只受当时的 HTTP 体上限约束,该说的仍然说得出,量级钉死在与挂载同阶。
|
|
44
43
|
*/
|
|
45
44
|
export declare const MAX_REQUEST_MCP_DROP_NAMES = 32;
|
|
46
45
|
/**
|
|
@@ -58,26 +57,20 @@ export declare const MAX_REQUEST_MCP_DROP_NAMES = 32;
|
|
|
58
57
|
* (唯一一格不在 core 型上的键 = http 传输的 `assertionHeader`:引擎尚无座,本仓承载 —— 那一支的门钉在
|
|
59
58
|
* `mcp-assertion-header.ts` 的 {@link RequestMcpServerSpec} 上,引擎一旦长出同名座,那里的退役门当场红。)
|
|
60
59
|
*
|
|
61
|
-
* ##
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
60
|
+
* ## 一个回答者,两扇门;读器另算
|
|
61
|
+
* 「这份声明能不能原样用」只有 {@link requestMcpDeclarationRefusal} 一只回答者:词表外的键(条目层 / 本形 `transport` 层)⇒ `400 request.body_shape` 带 `unknownKeys`,
|
|
62
|
+
* fresh 提交腿({@link assertRequestMcpEntries})与探测口同码同句 —— 一个拼错的键被悄悄不读,声明方会以为它生效了。
|
|
63
|
+
* 读器({@link normalizeServer})对词表外的键仍**宽容**(读不认识的键就当没有):它只服务重放存量 body 的腿,一个 4xx 打在 resume 上会永久砖死一条 parked 任务(本仓三处 `opts.leg` 先例)。
|
|
64
|
+
*
|
|
65
|
+
* ## 受理键的判官表也钉在这张表上
|
|
66
|
+
* 逐键判官表 {@link ENTRY_KEY_JUDGES} 是 `{ [K in 本表的键]: 判官 }` 的映射型 —— 本表多一个键而没写判官 ⇒ 编译红,不会出现「这个键写坏了却被悄悄受理」的新键。
|
|
67
67
|
*/
|
|
68
|
-
export declare const MCP_SERVER_ENTRY_KEYS: readonly ["allowTools", "contentOrigin", "elicitation", "name", "source", "toolAxes", "toolFaces", "transport"];
|
|
68
|
+
export declare const MCP_SERVER_ENTRY_KEYS: readonly ["allowTools", "contentOrigin", "elicitation", "name", "source", "startupTimeoutMs", "toolAxes", "toolFaces", "transport"];
|
|
69
69
|
/** `transport.kind:"stdio"` 的键闭集(两向钉在 core 那一支上)。 */
|
|
70
70
|
export declare const STDIO_MCP_TRANSPORT_KEYS: readonly ["args", "command", "env", "kind"];
|
|
71
71
|
/** `transport.kind:"http"` 的键闭集(同上)。`assertionHeader` 是本仓承载、引擎尚无座的一格(见 {@link RequestMcpServerSpec}):
|
|
72
72
|
* 表两向钉在**请求面的 http 传输型**上 = 引擎的键 ∪ 这一格;引擎一旦长出同名座,`mcp-assertion-header.ts` 的退役门当场红。 */
|
|
73
73
|
export declare const HTTP_MCP_TRANSPORT_KEYS: readonly ["assertionHeader", "headers", "kind", "principalHeader", "url"];
|
|
74
|
-
/** 条目层:`{}` 与缺席等价的可选 Record 键。 */
|
|
75
|
-
export declare const MCP_ENTRY_EMPTY_RECORD_KEYS: readonly ["toolAxes", "toolFaces"];
|
|
76
|
-
/** 条目层:`[]` 与缺席等价的可选数组键。 */
|
|
77
|
-
export declare const MCP_ENTRY_EMPTY_ARRAY_KEYS: readonly ["allowTools"];
|
|
78
|
-
/** 传输层(两种合起来):`{}` / `[]` 与缺席等价的键。 */
|
|
79
|
-
export declare const MCP_TRANSPORT_EMPTY_RECORD_KEYS: readonly ["env", "headers"];
|
|
80
|
-
export declare const MCP_TRANSPORT_EMPTY_ARRAY_KEYS: readonly ["args"];
|
|
81
74
|
/**
|
|
82
75
|
* Does THIS deployment honor caller-supplied per-request MCP servers? TRUE for a SINGLE-USER deployment
|
|
83
76
|
* (`requirePrincipal !== true`) — the requester is the super-admin of their own worker, so their `.mcp.json` is
|
|
@@ -145,97 +138,56 @@ type ValidatedRequestMcp = {
|
|
|
145
138
|
* 任务。同样是失败,同步 400 至少是**立刻可诊断**的那一种,而且拒得出理由(锁在哪、怎么改)。
|
|
146
139
|
*/
|
|
147
140
|
export declare function assertRequestMcpUnlocked(bodyMcp: unknown, lockedKeys: ReadonlySet<LockedKey>): void;
|
|
148
|
-
/**
|
|
149
|
-
* [ref](core 5.60.0 提货批件②)—— 请求体里 `mcpServers[].contentOrigin` 的**值域门**,腿首同步拒。
|
|
150
|
-
*
|
|
151
|
-
* ## 先把 core 的硬条款抄下来,因为本腿的全部争议都在它上面
|
|
152
|
-
* `McpServerSpec.contentOrigin` 顶注逐字:「TRUST SOURCE — a DEPLOYMENT-plane key … any assembly layer
|
|
153
|
-
* that accepts REQUEST-side MCP entries **must reject or strip this key** from them … A single-tenant
|
|
154
|
-
* superuser surface (a host reading its own `--mcp-config` file) **IS the deployment plane and needs no
|
|
155
|
-
* such gate**.」
|
|
156
|
-
*
|
|
157
|
-
* ## 本仓怎么兑现这条义务(**结构性**,不是靠纪律)
|
|
158
|
-
* 请求腿的条目只有在 {@link mcpInjectionHonored} 为真时才被读进 `TaskSpec.mcp`,而那只谓词的语义正是
|
|
159
|
-
* 「本部署是单用户 ⇒ 请求者就是这台 worker 的超管」—— core 点名豁免的那一格。多租户部署
|
|
160
|
-
* (`requirePrincipal===true`)整只 `mcpServers` 被忽略,租户**结构上**够不着这个键(钉见
|
|
161
|
-
* `test/task-mcp.test.ts` 的多租户格)。同一条腿今天已经透传 `toolAxes`(喂**审批门**,权限比本键**高**
|
|
162
|
-
* 一档 —— core 明写 contentOrigin「no gate/policy/roster effect」)与 stdio 的**任意命令**(worker 宿主
|
|
163
|
-
* 上的真无限制执行);在那两样之后,「我声明我自己这台 server 的内容不算外来」不构成任何新的暴露面。
|
|
164
|
-
* 名字撞车时 baseline 恒赢(`mergeRequestMcp`)⇒ 调用方也改写不了一台**已配置** server 的声明。
|
|
165
|
-
*
|
|
166
|
-
* ## 为什么 fresh 腿是 400 而不是静默丢键
|
|
167
|
-
* 静默丢键会把一次**信任边界声明**变成探针 —— 调用方以为声明生效了,其实没有。本仓 `opts.leg` 的成文
|
|
168
|
-
* 契约(`ServiceDeps.resolveSpec` 顶注)逐字写着 fresh 腿「may fail-loud 400 on combos that would
|
|
169
|
-
* silently discard caller payload」,这正是那种 combo。
|
|
170
|
-
*
|
|
171
|
-
* ## 🔴 但只在 fresh 腿(与 {@link assertRequestMcpUnlocked} **刻意分歧**)
|
|
172
|
-
* 锁那条敢在 resume 腿同拒,是因为它有一个本门**没有**的前提:锁在场 ⇒ core 的 preflight 无论如何都会
|
|
173
|
-
* 拒这条腿,放进去只换来一个跑起来又终态失败的任务。而坏 `contentOrigin` 在本仓两个平面都被**降级**掉了
|
|
174
|
-
* (`normalizeServer` / `resolveMcpServers`),core 根本看不到它,也就永远不会拒。
|
|
175
|
-
* 与此同时 `checkpoint_ctx` 存的是**原始** `TaskRequestBody`,耐久 resume 逐字重放它,从本门逃出去的
|
|
176
|
-
* `HttpError` 会被 `src/http/server.ts` 折成 `409 resume_blocked_by_policy` ⇒ 一条**升级前** park、body 里
|
|
177
|
-
* 带着当时被静默忽略的坏值的任务,升级后**永久**恢复不了(codex 交叉复审 R1-[high],验真后采纳)。
|
|
178
|
-
* ⇒ 照本仓成文的三处 `opts.leg` 先例(`appendSystemPrompt` 案 / `@model` 名单 [ref] 终裁 (a) /
|
|
179
|
-
* `compactionModel` 案,都逐字写着「砖死 resume 比降级糟」):fresh fail-loud,resume 降级 + 留声
|
|
180
|
-
* (留声点 = {@link resolveRequestMcp} 的 `mcp_content_class_dropped`)。挂点判据在 `boot/resolve-spec.ts`。
|
|
181
|
-
*
|
|
182
|
-
* ## 位置与射程
|
|
183
|
-
* 排在腿首、`assertRequestMcpUnlocked` 旁边 —— **任何副作用之前**(附件绑定 / per-session cwd·env 写入;
|
|
184
|
-
* 理由逐字见 `boot/resolve-spec.ts` 那两行的注)。判据是**纯值域**、与**租户车道**无关:一个写坏的请求
|
|
185
|
-
* 在哪条车道上都是写坏的,而多租户腿本来就要忽略整只数组,告诉调用方「你这个键写错了」只多不少。
|
|
186
|
-
* 形不合(非数组 / 条目非对象)**不归本门**——那是 `validateRequestMcp` 的既有宽容形,本门不抢裁决。
|
|
187
|
-
*/
|
|
188
|
-
export declare function assertRequestMcpContentOrigin(bodyMcp: unknown): void;
|
|
189
|
-
/**
|
|
190
|
-
* S-1515 —— `mcpServers[].transport`(http)上两个身份头的**值域门**(fresh 腿限定,同步 400):`principalHeader` 与 `assertionHeader`
|
|
191
|
-
* **同设**(`request.body_shape`),或 `assertionHeader` 在场而**不是字符串**(`request.field_invalid`)。
|
|
192
|
-
*
|
|
193
|
-
* 前者:两个键回答同一个问题(「这台 http 服务器的请求上用哪个头送身份」),同设 = 两个答案。settings 面由契约包在解析期判拒;
|
|
194
|
-
* 请求面在这里判拒 —— **同一条规则、两处执行者**,判据与拒句单点在 `mcp-assertion-header.ts`(对契约包原句逐字,差分钉守)。
|
|
195
|
-
* 后者:契约包的 zod 类型拒它;请求面若把它读成"没写过",一条鉴权声明就无声消失、server 却照常挂上去连。
|
|
196
|
-
*
|
|
197
|
-
* ## 位置与 leg 分流(与 {@link assertRequestMcpContentOrigin} 同款理由,不发明第二套)
|
|
198
|
-
* 排在腿首、任何副作用之前;**只在 fresh 腿**:`checkpoint_ctx` 存的是原始 `TaskRequestBody`,耐久 resume 逐字重放它,
|
|
199
|
-
* 从这里逃出去的 `HttpError` 会被折成 `409 resume_blocked_by_policy`,把一条 park 任务永久砖死。resume 腿走读器
|
|
200
|
-
* ({@link normalizeServer}):**降级到升级前的读法**(server 照挂、`principalHeader` 胜、`assertionHeader` 不生效)+ `classifyHonored` 一行 warn ——
|
|
201
|
-
* 不拒、也不摘 server。判据是纯值域、与租户车道无关(多租户腿本来就忽略整只数组,告诉调用方「你这条写错了」只多不少)。
|
|
202
|
-
*/
|
|
203
|
-
export declare function assertRequestMcpIdentityHeaders(bodyMcp: unknown): void;
|
|
204
141
|
/** 一条**原始**请求条目上两个身份头的问题类(没有问题 / 不是对象 / 不是 http 传输 ⇒ `undefined`)—— 提交腿的门、探测口、重放腿的披露三处共读,
|
|
205
142
|
* 判据本体在 `mcp-assertion-header.ts`;这里只负责把「条目 → 传输对象」那一步读出来。 */
|
|
206
143
|
export declare function requestEntryIdentityIssue(entry: unknown): McpIdentityHeadersIssue | undefined;
|
|
207
|
-
/**
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
144
|
+
/** 请求面 `mcpServers` 的一次拒:码 + 句 + 词表外的键名表(只在闭集那一形上在场)。两扇门(fresh 提交腿 / 探测口)同一份,状态恒 400。 */
|
|
145
|
+
export interface RequestMcpRefusal {
|
|
146
|
+
readonly errorCode: "request.body_shape" | "request.field_invalid";
|
|
147
|
+
readonly message: string;
|
|
148
|
+
/** 词表外的键,全路径形(`mcpServers[0].transport.header`;已清洗 + 截长 + 排序 + 封顶)。 */
|
|
149
|
+
readonly unknownKeys?: readonly string[];
|
|
150
|
+
}
|
|
213
151
|
/**
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
* ##
|
|
218
|
-
* `
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
152
|
+
* 请求面 `mcpServers`「这份声明能不能原样用」的**唯一回答者** —— fresh 提交腿({@link assertRequestMcpEntries})与探测口
|
|
153
|
+
* (`capabilities/mcp-probe.ts` 的 `readMcpProbeBody`)都问它、都原样转述它的答案(码与句逐字相同)。`undefined` = 能原样用,或列表缺席 / `null`(没声明;各门自己定缺席的意思)。
|
|
154
|
+
*
|
|
155
|
+
* ## 律:一个回答者、一个拒形,先列表后条目、条目里先键名后键值,第一处坏形即答
|
|
156
|
+
* ① **列表**:在场却不是数组 / 超过 {@link MAX_REQUEST_MCP_SERVERS} 条 / 某一条不是对象 ⇒ `400 request.field_invalid`,点名 `mcpServers` 或 `mcpServers[i]`(句尾同一句,见 {@link LIST_REFUSAL_TAIL})。
|
|
157
|
+
* 整份拒而不是挂前 32 条 / 摘掉读不出的那条 / 整只忽略:一份被悄悄截短、少了几条或整只没生效的声明,在发送方那里读起来是「生效了」。
|
|
158
|
+
* ② **键名**:条目层、或 `transport` 按 `kind` 那一形的键闭集之外的键 ⇒ `400 request.body_shape`,拒体带 `unknownKeys`(拼错的键被悄悄不读,同样读起来像生效了)。
|
|
159
|
+
* ③ **身份头的键组合**(`principalHeader` 与 `assertionHeader` 同设 / `assertionHeader` 非串)⇒ {@link requestEntryIdentityRefusal}(同设那一句对契约包解析期原句逐字)。
|
|
160
|
+
* ④ **受理键的值** ⇒ 逐键问判官表 {@link ENTRY_KEY_JUDGES},`400 request.field_invalid`,拒句点名 `mcpServers[i].<键路径>`、server 名与**该键判官的原因句**({@link entryFieldRefusal})。
|
|
161
|
+
* 判官**就是读器用的那一只**,不是第二份:`toolAxes` = 引擎根导出的 `readMcpToolAxes`;`toolFaces` = `readMcpToolFaces`;`contentOrigin` = `isContentOriginWord`;`source` = `isMcpSourceLabel`;
|
|
162
|
+
* `name` = `isUsableWireName`;`allowTools` / `transport.args` = {@link isStringArray};`transport.env` / `headers` = {@link readPlainStringRecord};`elicitation` = {@link isElicitationFlag};
|
|
163
|
+
* `transport` 的硬字段(`kind` / `command` / `url`)与软字段(`args` / `env` / `headers` / `principalHeader`)= {@link readRequestTransport}(读器投影 transport 的那一只函数,门读它报的 `lost`)。
|
|
164
|
+
* 判官只在引擎里、包根没给读口的键(今天只有 `startupTimeoutMs`)不在这里判,原样过境、由引擎在拨号时按 server 拒(不写第二份判据;到货读口即换)。
|
|
165
|
+
* 于是判官受理的条目,读器({@link normalizeServer})交回的就是申报本身(只差判官自己的合法归一:空容器 / `skillScopeEligible:false` 折成缺席、`toolAxes` 取引擎的拷贝)——
|
|
166
|
+
* 探测口拨的那一份因此就是提交腿会挂的那一份,不另比一遍(另比就是第二只回答者)。唯一按门分的后果是「声明了 `assertionHeader`」这一个事实(引擎尚无座,
|
|
167
|
+
* 承载、不发送):新提交照挂 + 运维 info,探测口整条拒 —— 判据是同一只谓词 `declaresMcpAssertionHeader`,不是第二份判法(见 `readMcpProbeBody`)。
|
|
168
|
+
*
|
|
169
|
+
* ## 射程
|
|
170
|
+
* 纯值域、与租户车道无关(一个写坏的请求在哪条车道上都是写坏的;多租户腿本来就忽略整只数组,告诉调用方「你写错了」只多不少)。
|
|
171
|
+
* 重放存量 body 的腿**不问它**:那条腿走宽容读器(见 {@link assertRequestMcpEntries} 的 leg 分流)。
|
|
172
|
+
* 关于 `contentOrigin` 的信任语义(core 把它定性为**部署面**键):请求腿的条目只有在 {@link mcpInjectionHonored} 为真(单用户 = 请求者就是这台 worker 的超管)时才被读进 `TaskSpec.mcp`,
|
|
173
|
+
* 多租户部署整只数组被忽略 ⇒ 租户**结构上**够不着这个键;名字撞车时 baseline 恒赢({@link mergeRequestMcp})⇒ 调用方也改写不了一台**已配置** server 的声明。
|
|
228
174
|
*/
|
|
229
|
-
export declare function
|
|
175
|
+
export declare function requestMcpDeclarationRefusal(list: unknown): RequestMcpRefusal | undefined;
|
|
230
176
|
/**
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
177
|
+
* fresh 提交腿的 `mcpServers` 门:{@link requestMcpDeclarationRefusal} 有答案 ⇒ 同步 400(码 / 句 / `unknownKeys` 原样)。
|
|
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} / {@link classifyRequestMcp}),处置:整只不是数组 ⇒ 整只忽略 + 运维 warn `mcp_injection_ignored`;条目不是对象 / 坏硬键
|
|
185
|
+
* (`name` / `transport` 硬字段 / `allowTools` / `toolAxes` / `toolFaces`)⇒ 这一条 server 整条不进 + 运维 warn `mcp_injection_dropped` + 用户面通告 `mcp.injection_dropped` 点名 `field`
|
|
186
|
+
* (`toolAxes` 在一次性迁移 M19 的窗开着时先按升级前读法重读一次,过了就挂,见读器的注);超上限的尾项 ⇒ `over_cap` 点名;坏软键(`contentOrigin` / `elicitation` / `source`)
|
|
187
|
+
* 与词表外的键 ⇒ 丢键、server 照挂(`contentOrigin` 另有 `mcp_content_class_dropped` 留声);`transport` 软字段 ⇒ 只不交坏的那几处(`env` / `headers` 对 JSON 存体按 7.131.x 的判据整键交或不交,
|
|
188
|
+
* 交出去的剔掉 `__proto__` 名成员),server 照挂,运维 warn `mcp_transport_members_dropped` 逐处点名 —— 续跑重放腿不静默丢声明。
|
|
234
189
|
*/
|
|
235
|
-
export declare function
|
|
236
|
-
errorCode: "request.field_invalid";
|
|
237
|
-
message: string;
|
|
238
|
-
} | undefined;
|
|
190
|
+
export declare function assertRequestMcpEntries(bodyMcp: unknown): void;
|
|
239
191
|
/**
|
|
240
192
|
* The ONE predicate for "this MCP server name belongs to the deployment": the scenario baseline (what mounted for
|
|
241
193
|
* this scenario) ∪ every name the deployment DECLARED (`CenterMcp.reservedNames` — mounted or not, any scenario).
|
package/dist/task-mcp.js
CHANGED
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
import { mcpInjectionDroppedNotice, readMcpToolAxes, resolveComplianceDenies } from "@sema-agent/core";
|
|
2
|
-
import { isUsableWireName } from "
|
|
2
|
+
import { MCP_CONTENT_ORIGINS, describeNotStringArray, describeValueShape, isContentOriginWord, isStringArray, isUsableWireName, readMcpIdentityHeaders, } from "@sema-agent/settings-schema";
|
|
3
|
+
import { describeBadContentOrigin, describeBadWireName } from "./caller-text-echo.js";
|
|
3
4
|
import { HttpError, createCodedHttpError } from "./security.js";
|
|
4
5
|
import { GOVERNANCE_HTTP_STATUS } from "./governance-http-status.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { echoKeyName } from "./request-key-closure.js";
|
|
6
|
+
import { MCP_SOURCE_LABEL_MAX, isMcpSourceLabel, readMcpSourceLabel } from "./mcp-source-label.js";
|
|
7
|
+
import { declaresMcpAssertionHeader, describeMcpAssertionHeaderNotAString, describeMcpIdentityHeadersConflict, mcpIdentityHeadersIssue, noteMcpAssertionHeaderIgnored, noteMcpAssertionHeaderNotSent, toCoreMcpServerSpec } from "./mcp-assertion-header.js";
|
|
8
|
+
import { MAX_KEY_ECHO, echoKeyName, keyClosureClause, scanKeyClosure } from "./request-key-closure.js";
|
|
9
9
|
import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
|
|
10
10
|
import { readMcpToolFaces } from "./mcp-tool-faces.js";
|
|
11
|
+
import { readPlainStringRecord } from "./plain-string-record.js";
|
|
11
12
|
export const MAX_REQUEST_MCP_SERVERS = 32;
|
|
12
13
|
export const MAX_REQUEST_MCP_DROP_NAMES = MAX_REQUEST_MCP_SERVERS;
|
|
13
|
-
export const MCP_SERVER_ENTRY_KEYS = ["allowTools", "contentOrigin", "elicitation", "name", "source", "toolAxes", "toolFaces", "transport"];
|
|
14
|
+
export const MCP_SERVER_ENTRY_KEYS = ["allowTools", "contentOrigin", "elicitation", "name", "source", "startupTimeoutMs", "toolAxes", "toolFaces", "transport"];
|
|
14
15
|
export const STDIO_MCP_TRANSPORT_KEYS = ["args", "command", "env", "kind"];
|
|
15
16
|
export const HTTP_MCP_TRANSPORT_KEYS = ["assertionHeader", "headers", "kind", "principalHeader", "url"];
|
|
16
|
-
export const MCP_ENTRY_EMPTY_RECORD_KEYS = ["toolAxes", "toolFaces"];
|
|
17
|
-
export const MCP_ENTRY_EMPTY_ARRAY_KEYS = ["allowTools"];
|
|
18
|
-
export const MCP_TRANSPORT_EMPTY_RECORD_KEYS = ["env", "headers"];
|
|
19
|
-
export const MCP_TRANSPORT_EMPTY_ARRAY_KEYS = ["args"];
|
|
20
17
|
export function mcpInjectionHonored(config) {
|
|
21
18
|
if (config.lockedConfigKeys?.includes("mcp") === true)
|
|
22
19
|
return false;
|
|
@@ -24,8 +21,67 @@ export function mcpInjectionHonored(config) {
|
|
|
24
21
|
return false;
|
|
25
22
|
return config.requirePrincipal !== true;
|
|
26
23
|
}
|
|
27
|
-
function
|
|
28
|
-
return
|
|
24
|
+
function isRecord(v) {
|
|
25
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
26
|
+
}
|
|
27
|
+
function isElicitationFlag(v) {
|
|
28
|
+
return typeof v === "boolean";
|
|
29
|
+
}
|
|
30
|
+
const transportBad = (path, reason) => ({ ok: false, issue: { path, reason } });
|
|
31
|
+
function readRequestTransport(t) {
|
|
32
|
+
if (!t || typeof t !== "object")
|
|
33
|
+
return transportBad("transport", `must be an object with a "kind" of "stdio" or "http" — got ${describeValueShape(t)}`);
|
|
34
|
+
const tr = t;
|
|
35
|
+
const lost = [];
|
|
36
|
+
if (tr.kind === "stdio") {
|
|
37
|
+
if (typeof tr.command !== "string" || tr.command.length === 0)
|
|
38
|
+
return transportBad("transport.command", `must be a non-empty string (the command to run) — got ${describeValueShape(tr.command)}`);
|
|
39
|
+
if (tr.args !== undefined && !isStringArray(tr.args))
|
|
40
|
+
lost.push({ path: "transport.args", reason: `must be an array of strings — got ${describeNotStringArray(tr.args)}` });
|
|
41
|
+
const env = readTransportRecord("transport.env", tr.env, lost);
|
|
42
|
+
return {
|
|
43
|
+
ok: true,
|
|
44
|
+
transport: {
|
|
45
|
+
kind: "stdio",
|
|
46
|
+
command: tr.command,
|
|
47
|
+
...(isStringArray(tr.args) ? { args: tr.args } : {}),
|
|
48
|
+
...(env !== undefined ? { env } : {}),
|
|
49
|
+
},
|
|
50
|
+
lost,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (tr.kind === "http") {
|
|
54
|
+
if (typeof tr.url !== "string" || !/^https?:\/\//i.test(tr.url))
|
|
55
|
+
return transportBad("transport.url", "must be an absolute http(s) URL (put credentials in transport.headers, never in the URL)");
|
|
56
|
+
const ids = readMcpIdentityHeaders(tr);
|
|
57
|
+
const headers = readTransportRecord("transport.headers", tr.headers, lost);
|
|
58
|
+
if (tr.principalHeader !== undefined && ids.principalHeader === undefined) {
|
|
59
|
+
lost.push({ path: "transport.principalHeader", reason: `must be a string (the HTTP header NAME the end-user principal is injected into) — got ${describeValueShape(tr.principalHeader)}` });
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
ok: true,
|
|
63
|
+
transport: {
|
|
64
|
+
kind: "http",
|
|
65
|
+
url: tr.url,
|
|
66
|
+
...(headers !== undefined ? { headers } : {}),
|
|
67
|
+
...ids,
|
|
68
|
+
},
|
|
69
|
+
lost,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return transportBad("transport.kind", `must be "stdio" or "http" — got ${describeValueShape(tr.kind)}`);
|
|
73
|
+
}
|
|
74
|
+
function readTransportRecord(path, raw, lost) {
|
|
75
|
+
if (raw === undefined)
|
|
76
|
+
return undefined;
|
|
77
|
+
const read = readPlainStringRecord(raw);
|
|
78
|
+
if (!read.ok) {
|
|
79
|
+
lost.push({ path, reason: `must be a plain object of string values — ${read.member === undefined ? read.fault : `its member "${echoKeyName("", read.member)}" ${read.fault}`}` });
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
for (const m of read.stripped)
|
|
83
|
+
lost.push({ path: echoKeyName(`${path}.`, m.name), reason: m.fault });
|
|
84
|
+
return read.record;
|
|
29
85
|
}
|
|
30
86
|
const bad = (field) => ({ ok: false, field });
|
|
31
87
|
const MALFORMED_NAME = "<malformed>";
|
|
@@ -40,47 +96,22 @@ function normalizeServer(e, noteBadOrigin, storedToolAxes) {
|
|
|
40
96
|
const o = e;
|
|
41
97
|
if (!isUsableWireName(o.name))
|
|
42
98
|
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 };
|
|
99
|
+
const read = readRequestTransport(o.transport);
|
|
100
|
+
if (!read.ok)
|
|
101
|
+
return bad(read.issue.path);
|
|
102
|
+
const spec = { name: o.name, transport: read.transport };
|
|
72
103
|
if (o.allowTools !== undefined) {
|
|
73
|
-
if (!
|
|
104
|
+
if (!isStringArray(o.allowTools))
|
|
74
105
|
return bad("allowTools");
|
|
75
106
|
spec.allowTools = o.allowTools;
|
|
76
107
|
}
|
|
77
|
-
if (
|
|
108
|
+
if (isElicitationFlag(o.elicitation))
|
|
78
109
|
spec.elicitation = o.elicitation;
|
|
79
110
|
const axes = readMcpToolAxes(o.toolAxes);
|
|
80
111
|
let judged = axes;
|
|
81
112
|
let toolAxesReread;
|
|
82
113
|
if (!axes.ok && storedToolAxes !== undefined) {
|
|
83
|
-
const again = readMcpToolAxes(storedToolAxes(o.toolAxes));
|
|
114
|
+
const again = readMcpToolAxes(storedToolAxes.mcp(o.toolAxes));
|
|
84
115
|
if (again.ok) {
|
|
85
116
|
judged = again;
|
|
86
117
|
toolAxesReread = axes.problem.replace(CONTROL_AND_BIDI_STRIP_RE, "");
|
|
@@ -97,12 +128,14 @@ function normalizeServer(e, noteBadOrigin, storedToolAxes) {
|
|
|
97
128
|
noteBadOrigin(spec.name, o.contentOrigin);
|
|
98
129
|
}
|
|
99
130
|
Object.assign(spec, readMcpSourceLabel(spec.name, o.source));
|
|
131
|
+
if (o.startupTimeoutMs !== undefined)
|
|
132
|
+
spec.startupTimeoutMs = o.startupTimeoutMs;
|
|
100
133
|
const faces = readMcpToolFaces(o.toolFaces);
|
|
101
134
|
if (!faces.ok)
|
|
102
135
|
return bad("toolFaces");
|
|
103
136
|
if (faces.faces !== undefined)
|
|
104
137
|
spec.toolFaces = faces.faces;
|
|
105
|
-
return { ok: true, spec, ...(toolAxesReread !== undefined ? { toolAxesReread } : {}) };
|
|
138
|
+
return { ok: true, spec, ...(toolAxesReread !== undefined ? { toolAxesReread } : {}), transportLost: read.lost };
|
|
106
139
|
}
|
|
107
140
|
export function validateRequestMcp(raw) {
|
|
108
141
|
if (raw !== undefined && raw !== null && !Array.isArray(raw))
|
|
@@ -115,19 +148,22 @@ function validateRequestMcpEntries(raw, storedToolAxes) {
|
|
|
115
148
|
const dropped = [];
|
|
116
149
|
const badContentOrigin = [];
|
|
117
150
|
const toolAxesReread = [];
|
|
151
|
+
const transportLost = [];
|
|
118
152
|
for (const e of raw.slice(0, MAX_REQUEST_MCP_SERVERS)) {
|
|
119
153
|
const v = normalizeServer(e, (server, value) => badContentOrigin.push({ server, value: describeBadContentOrigin(value) }), storedToolAxes);
|
|
120
154
|
if (v.ok) {
|
|
121
155
|
ok.push(v.spec);
|
|
122
156
|
if (v.toolAxesReread !== undefined)
|
|
123
157
|
toolAxesReread.push({ server: v.spec.name, problem: v.toolAxesReread });
|
|
158
|
+
if (v.transportLost.length > 0)
|
|
159
|
+
transportLost.push({ server: v.spec.name, lost: v.transportLost.map((i) => i.path) });
|
|
124
160
|
}
|
|
125
161
|
else
|
|
126
162
|
dropped.push({ server: declaredName(e), reason: "malformed_entry", field: v.field });
|
|
127
163
|
}
|
|
128
164
|
for (const e of raw.slice(MAX_REQUEST_MCP_SERVERS, MAX_REQUEST_MCP_SERVERS + MAX_REQUEST_MCP_DROP_NAMES))
|
|
129
165
|
dropped.push({ server: declaredName(e), reason: "over_cap" });
|
|
130
|
-
return { ok, dropped, droppedTotal: Math.max(0, raw.length - ok.length), badContentOrigin, toolAxesReread };
|
|
166
|
+
return { ok, dropped, droppedTotal: Math.max(0, raw.length - ok.length), badContentOrigin, toolAxesReread, transportLost };
|
|
131
167
|
}
|
|
132
168
|
export function assertRequestMcpUnlocked(bodyMcp, lockedKeys) {
|
|
133
169
|
if (!lockedKeys.has("mcp"))
|
|
@@ -138,30 +174,6 @@ export function assertRequestMcpUnlocked(bodyMcp, lockedKeys) {
|
|
|
138
174
|
code: "config.locked_key",
|
|
139
175
|
});
|
|
140
176
|
}
|
|
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
177
|
export function requestEntryIdentityIssue(entry) {
|
|
166
178
|
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
167
179
|
return undefined;
|
|
@@ -170,39 +182,117 @@ export function requestEntryIdentityIssue(entry) {
|
|
|
170
182
|
return undefined;
|
|
171
183
|
return mcpIdentityHeadersIssue(t);
|
|
172
184
|
}
|
|
173
|
-
|
|
185
|
+
function requestEntryIdentityRefusal(entry, at) {
|
|
174
186
|
const issue = requestEntryIdentityIssue(entry);
|
|
175
|
-
if (issue === undefined)
|
|
187
|
+
if (issue === undefined || !isRecord(entry))
|
|
176
188
|
return undefined;
|
|
177
|
-
const name = echoKeyName("", declaredName(entry));
|
|
178
189
|
if (issue === "both_set")
|
|
179
|
-
return { errorCode: "request.body_shape", message: describeMcpIdentityHeadersConflict(
|
|
180
|
-
const
|
|
181
|
-
|
|
190
|
+
return { errorCode: "request.body_shape", message: describeMcpIdentityHeadersConflict(echoKeyName("", declaredName(entry))) };
|
|
191
|
+
const transport = entry.transport;
|
|
192
|
+
const value = isRecord(transport) ? transport.assertionHeader : undefined;
|
|
193
|
+
return entryFieldRefusal(entry, at, { path: "transport.assertionHeader", reason: describeMcpAssertionHeaderNotAString(value) });
|
|
182
194
|
}
|
|
183
|
-
export function
|
|
184
|
-
if (
|
|
185
|
-
return;
|
|
186
|
-
|
|
187
|
-
|
|
195
|
+
export function requestMcpDeclarationRefusal(list) {
|
|
196
|
+
if (list === undefined || list === null)
|
|
197
|
+
return undefined;
|
|
198
|
+
if (!Array.isArray(list))
|
|
199
|
+
return listRefusal(`mcpServers is ${describeValueShape(list)}, not an array of MCP server declarations`);
|
|
200
|
+
if (list.length > MAX_REQUEST_MCP_SERVERS)
|
|
201
|
+
return listRefusal(`mcpServers carries ${list.length} declarations, above this deployment's ceiling of ${MAX_REQUEST_MCP_SERVERS}`);
|
|
202
|
+
for (let i = 0; i < list.length; i++) {
|
|
203
|
+
const refusal = requestEntryRefusal(list[i], `mcpServers[${i}]`);
|
|
188
204
|
if (refusal !== undefined)
|
|
189
|
-
|
|
205
|
+
return refusal;
|
|
190
206
|
}
|
|
207
|
+
return undefined;
|
|
191
208
|
}
|
|
192
|
-
export function
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
209
|
+
export function assertRequestMcpEntries(bodyMcp) {
|
|
210
|
+
const refusal = requestMcpDeclarationRefusal(bodyMcp);
|
|
211
|
+
if (refusal !== undefined)
|
|
212
|
+
throw createCodedHttpError(400, refusal.errorCode, refusal.message, refusal.unknownKeys === undefined ? undefined : { unknownKeys: [...refusal.unknownKeys] });
|
|
213
|
+
}
|
|
214
|
+
const LIST_REFUSAL_TAIL = "The request is refused rather than acting on part of the list: declarations that silently did not apply would read as in effect to whoever sent them. " +
|
|
215
|
+
"Fix the list (an empty list declares none).";
|
|
216
|
+
function listRefusal(head) {
|
|
217
|
+
return { errorCode: "request.field_invalid", message: `${head}. ${LIST_REFUSAL_TAIL}` };
|
|
218
|
+
}
|
|
219
|
+
const ENTRY_KEY_SET = new Set(MCP_SERVER_ENTRY_KEYS);
|
|
220
|
+
const STDIO_TRANSPORT_KEY_SET = new Set(STDIO_MCP_TRANSPORT_KEYS);
|
|
221
|
+
const HTTP_TRANSPORT_KEY_SET = new Set(HTTP_MCP_TRANSPORT_KEYS);
|
|
222
|
+
const NO_TOLERATED_KEYS = new Set();
|
|
223
|
+
function entryUnknownKeys(entry, at) {
|
|
224
|
+
const buckets = { unknown: [], unsupported: [] };
|
|
225
|
+
scanKeyClosure(entry, `${at}.`, ENTRY_KEY_SET, NO_TOLERATED_KEYS, buckets);
|
|
226
|
+
const t = entry.transport;
|
|
227
|
+
if (isRecord(t)) {
|
|
228
|
+
const keys = t.kind === "stdio" ? STDIO_TRANSPORT_KEY_SET : t.kind === "http" ? HTTP_TRANSPORT_KEY_SET : undefined;
|
|
229
|
+
if (keys !== undefined)
|
|
230
|
+
scanKeyClosure(t, `${at}.transport.`, keys, NO_TOLERATED_KEYS, buckets);
|
|
231
|
+
}
|
|
232
|
+
return buckets.unknown.sort();
|
|
233
|
+
}
|
|
234
|
+
const ENTRY_KEY_JUDGES = {
|
|
235
|
+
allowTools: (o) => o.allowTools === undefined || isStringArray(o.allowTools)
|
|
236
|
+
? undefined
|
|
237
|
+
: { path: "allowTools", reason: `must be an array of strings (omit the key to leave the server's tools unrestricted) — got ${describeNotStringArray(o.allowTools)}` },
|
|
238
|
+
contentOrigin: (o) => o.contentOrigin === undefined || isContentOriginWord(o.contentOrigin)
|
|
239
|
+
? undefined
|
|
240
|
+
: {
|
|
241
|
+
path: "contentOrigin",
|
|
242
|
+
reason: `must be one of ${MCP_CONTENT_ORIGINS.map((w) => `"${w}"`).join(" | ")} — got ${describeBadContentOrigin(o.contentOrigin)}` +
|
|
243
|
+
` (omit the key for the default: this server's tools classify as external content)`,
|
|
244
|
+
},
|
|
245
|
+
elicitation: (o) => (o.elicitation === undefined || isElicitationFlag(o.elicitation) ? undefined : { path: "elicitation", reason: `must be a boolean — got ${describeValueShape(o.elicitation)}` }),
|
|
246
|
+
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)}` }),
|
|
247
|
+
source: (o) => o.source === undefined || isMcpSourceLabel(o.source)
|
|
248
|
+
? undefined
|
|
249
|
+
: { path: "source", reason: `must be a non-empty string of at most ${MCP_SOURCE_LABEL_MAX} characters (a display label) — got ${describeValueShape(o.source)}` },
|
|
250
|
+
startupTimeoutMs: () => undefined,
|
|
251
|
+
toolAxes: (o) => {
|
|
252
|
+
const read = readMcpToolAxes(o.toolAxes);
|
|
253
|
+
return read.ok ? undefined : { path: "toolAxes", reason: read.problem };
|
|
254
|
+
},
|
|
255
|
+
toolFaces: (o) => {
|
|
256
|
+
const read = readMcpToolFaces(o.toolFaces);
|
|
257
|
+
return read.ok ? undefined : { path: "toolFaces", reason: read.problem };
|
|
258
|
+
},
|
|
259
|
+
transport: (o) => {
|
|
260
|
+
const read = readRequestTransport(o.transport);
|
|
261
|
+
return read.ok ? read.lost[0] : read.issue;
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
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. " +
|
|
265
|
+
"Fix the entry (an optional key may also be omitted).";
|
|
266
|
+
function entryFieldRefusal(entry, at, issue) {
|
|
198
267
|
const server = echoKeyName("", declaredName(entry));
|
|
199
268
|
return {
|
|
200
269
|
errorCode: "request.field_invalid",
|
|
201
|
-
message: `${at}.
|
|
202
|
-
`The whole declaration is refused: the tools that were fine are not kept and nothing is defaulted, because a toolAxes declaration is your own risk-axis judgement for this server's tools ` +
|
|
203
|
-
`(it can move a tool down or up), and a half-applied one would silently differ from what you declared. Fix the entry, or omit the key to declare nothing.`,
|
|
270
|
+
message: `${at}.${issue.path} (server "${server}") is not a usable declaration — ${issue.reason.replace(CONTROL_AND_BIDI_STRIP_RE, "")}. ${ENTRY_REFUSAL_TAIL}`,
|
|
204
271
|
};
|
|
205
272
|
}
|
|
273
|
+
function requestEntryRefusal(entry, at) {
|
|
274
|
+
if (!isRecord(entry))
|
|
275
|
+
return listRefusal(`${at} is ${describeValueShape(entry)}, not an MCP server declaration (an object)`);
|
|
276
|
+
const unknownKeys = entryUnknownKeys(entry, at);
|
|
277
|
+
if (unknownKeys.length > 0) {
|
|
278
|
+
return {
|
|
279
|
+
errorCode: "request.body_shape",
|
|
280
|
+
message: `body: unrecognized key(s) on an MCP declaration — refused rather than dropped, because a dropped key reads as "in effect" to the caller. ` +
|
|
281
|
+
`${keyClosureClause(unknownKeys, "unknown (not an mcpServers[] key — check the spelling)")}. ` +
|
|
282
|
+
`Accepted per entry: ${[...MCP_SERVER_ENTRY_KEYS].sort().join(",")} (transport.kind "stdio": ${[...STDIO_MCP_TRANSPORT_KEYS].sort().join(",")}; "http": ${[...HTTP_MCP_TRANSPORT_KEYS].sort().join(",")})`,
|
|
283
|
+
unknownKeys: unknownKeys.slice(0, MAX_KEY_ECHO),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
const identity = requestEntryIdentityRefusal(entry, at);
|
|
287
|
+
if (identity !== undefined)
|
|
288
|
+
return identity;
|
|
289
|
+
for (const key of MCP_SERVER_ENTRY_KEYS) {
|
|
290
|
+
const issue = ENTRY_KEY_JUDGES[key](entry);
|
|
291
|
+
if (issue !== undefined)
|
|
292
|
+
return entryFieldRefusal(entry, at, issue);
|
|
293
|
+
}
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
206
296
|
export function deploymentClosedMcpNames(baseline, reservedNames) {
|
|
207
297
|
return new Set([...(baseline ?? []).map((s) => s.name), ...reservedNames]);
|
|
208
298
|
}
|
|
@@ -243,7 +333,7 @@ function classifyHonored(baseline, entries, config, logger, storedToolAxes) {
|
|
|
243
333
|
if (v.badContentOrigin.length > 0) {
|
|
244
334
|
logger?.warn("mcp_content_class_dropped", {
|
|
245
335
|
entries: v.badContentOrigin,
|
|
246
|
-
allowed:
|
|
336
|
+
allowed: MCP_CONTENT_ORIGINS,
|
|
247
337
|
note: "declaration DROPPED — these servers keep the pre-378 default (external content, session gets marked); the servers themselves still mount",
|
|
248
338
|
});
|
|
249
339
|
}
|
|
@@ -269,6 +359,18 @@ function classifyHonored(baseline, entries, config, logger, storedToolAxes) {
|
|
|
269
359
|
note: "this replayed request was stored before the upgrade and its toolAxes declaration does not pass the engine's reader; while one-time migration M19 is open it is read the way the pre-upgrade server read it (members that did not fit are dropped, a declaration that is not an object counts as none) and the server stays mounted — once the window closes the same request leaves this server unmounted",
|
|
270
360
|
});
|
|
271
361
|
}
|
|
362
|
+
for (const t of v.transportLost) {
|
|
363
|
+
if (!mounted.has(t.server))
|
|
364
|
+
continue;
|
|
365
|
+
logger?.warn("mcp_transport_members_dropped", {
|
|
366
|
+
server: t.server,
|
|
367
|
+
lost: t.lost.slice(0, MAX_KEY_ECHO),
|
|
368
|
+
...(t.lost.length > MAX_KEY_ECHO ? { lostTotal: t.lost.length } : {}),
|
|
369
|
+
note: "this replayed request carries parts of this server's transport declaration that cannot be handed to the engine — an env / headers record that is not a plain object of string values (left out whole, as the pre-upgrade server read it), " +
|
|
370
|
+
"a member named \"__proto__\" in env / headers (only that member is left out: the engine refuses the name), args that are not a list of strings, a principalHeader that is not a string; " +
|
|
371
|
+
"the server mounts with the rest. A new submission with the same entry is refused (400)",
|
|
372
|
+
});
|
|
373
|
+
}
|
|
272
374
|
return {
|
|
273
375
|
mcp: mergeRequestMcp(baseline, v.ok.map(toCoreMcpServerSpec), reservedNames),
|
|
274
376
|
drops: [...v.dropped, ...blocked.map((server) => ({ server, reason: "name_reserved_by_deployment" }))],
|