@sema-agent/server 7.137.0 → 7.138.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 +328 -139
- package/README.md +1 -0
- package/README.zh-CN.md +1 -0
- package/USAGE.md +67 -20
- package/dist/approval-card.d.ts +21 -14
- package/dist/approval-card.js +1 -0
- package/dist/audit.js +1 -0
- package/dist/background-agent-name.d.ts +5 -0
- package/dist/background-agent-name.js +5 -0
- package/dist/boot/budget-tracing.d.ts +1 -12
- package/dist/boot/deployment-mods.d.ts +46 -16
- package/dist/boot/deployment-mods.js +82 -6
- package/dist/boot/governance-seams.d.ts +10 -17
- package/dist/boot/governance-seams.js +5 -17
- package/dist/boot/leader.d.ts +3 -0
- package/dist/boot/leader.js +1 -0
- package/dist/boot/org-delivery.js +15 -2
- package/dist/boot/resolve-spec.d.ts +3 -0
- package/dist/boot/resolve-spec.js +23 -12
- package/dist/boot/retention-lane.d.ts +11 -5
- package/dist/boot/retention-lane.js +2 -2
- package/dist/boot/runner-deps.d.ts +7 -3
- package/dist/boot/runner-deps.js +1 -0
- package/dist/boot/stage-04-stores.d.ts +1 -12
- package/dist/boot/stage-05-execution-env.d.ts +3 -12
- package/dist/boot/stage-05-execution-env.js +7 -4
- package/dist/boot/stage-06-runners.d.ts +3 -12
- package/dist/boot/stage-06-runners.js +5 -2
- package/dist/boot/stage-07-capability-layer.d.ts +3 -12
- package/dist/boot/stage-07-capability-layer.js +2 -2
- package/dist/boot/stage-08-reapers.d.ts +3 -12
- package/dist/boot/stage-09-leader.d.ts +3 -12
- package/dist/boot/stage-09-leader.js +2 -2
- package/dist/boot/stage-10-http-server.d.ts +3 -12
- package/dist/boot/stage-10-http-server.js +4 -2
- package/dist/boot/web-admin.d.ts +5 -2
- package/dist/boot/web-admin.js +3 -3
- package/dist/brain.js +5 -5
- package/dist/budget.d.ts +17 -15
- package/dist/budget.js +2 -2
- package/dist/capabilities/mcp-probe.d.ts +36 -11
- package/dist/capabilities/mcp-probe.js +44 -5
- package/dist/capability-facts.d.ts +6 -0
- package/dist/capability-facts.js +5 -0
- package/dist/checkpoint-identity.d.ts +13 -6
- package/dist/config-catalog.js +7 -1
- package/dist/config-types.d.ts +15 -7
- package/dist/config.js +8 -5
- package/dist/deployment-shape.d.ts +6 -6
- package/dist/gate-settlement.d.ts +71 -0
- package/dist/gate-settlement.js +40 -0
- package/dist/hooks/classic-stations.d.ts +1 -0
- package/dist/hooks/classic-stations.js +1 -0
- package/dist/hooks/command-spawn.d.ts +2 -2
- package/dist/hooks/hook-runner.js +14 -14
- package/dist/http/admission.js +4 -2
- package/dist/http/dispatch.d.ts +4 -0
- package/dist/http/dispatch.js +3 -0
- package/dist/http/durable-run-leg.js +4 -2
- package/dist/http/memory-compliance-failure-reply.js +3 -0
- package/dist/http/resume-legs.d.ts +5 -11
- package/dist/http/resume-legs.js +136 -91
- package/dist/http/route-ctx.d.ts +30 -51
- package/dist/http/route-ctx.js +2 -5
- package/dist/http/routes/a2a-serve.js +10 -4
- package/dist/http/routes/agents-roster.d.ts +1 -1
- package/dist/http/routes/agents-roster.js +5 -1
- package/dist/http/routes/approvals-assistant.js +6 -6
- package/dist/http/routes/capabilities.js +12 -10
- package/dist/http/routes/mcp-reconnect.d.ts +3 -2
- package/dist/http/routes/mcp-reconnect.js +7 -5
- package/dist/http/routes/memory-consolidation.d.ts +1 -1
- package/dist/http/routes/memory-origin.d.ts +1 -1
- package/dist/http/routes/rules.js +3 -3
- package/dist/http/routes/runs.js +11 -7
- package/dist/http/routes/sessions.js +6 -4
- package/dist/http/routes/side-query.js +2 -3
- package/dist/http/routes/tasks.js +792 -780
- package/dist/http/server.d.ts +31 -25
- package/dist/http/wire-types.d.ts +14 -6
- package/dist/identity-source.d.ts +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/leader/wire.d.ts +7 -0
- package/dist/leader/wire.js +5 -4
- package/dist/mcp-assertion-header.d.ts +51 -40
- package/dist/mcp-assertion-header.js +10 -11
- package/dist/mcp-authorization-face.d.ts +27 -0
- package/dist/mcp-authorization-face.js +15 -0
- package/dist/memory-operator-faces.d.ts +13 -4
- package/dist/memory-operator-faces.js +1 -17
- package/dist/memory-sync-client.js +72 -57
- package/dist/memory-sync.d.ts +1 -1
- package/dist/model-route-gap.d.ts +6 -22
- package/dist/model-route-gap.js +7 -5
- package/dist/mods-declaration.d.ts +110 -0
- package/dist/mods-declaration.js +109 -0
- package/dist/observability/err-text.d.ts +5 -0
- package/dist/observability/err-text.js +9 -0
- package/dist/observability/fail-open.d.ts +5 -1
- package/dist/observability/fail-open.js +5 -1
- package/dist/observability/secret-env-scrub.d.ts +1 -1
- package/dist/observability/secret-env-scrub.js +1 -1
- package/dist/parked-decide.d.ts +15 -2
- package/dist/parked-decide.js +14 -8
- package/dist/peer-seat.d.ts +150 -0
- package/dist/peer-seat.js +239 -0
- package/dist/permission-rule-vocab.d.ts +7 -7
- package/dist/permission-rule-vocab.js +3 -3
- package/dist/plain-string-record.d.ts +38 -0
- package/dist/plain-string-record.js +38 -0
- package/dist/plugins/background-agent-store-sql.js +6 -3
- package/dist/plugins/caching-session-store.d.ts +5 -0
- package/dist/plugins/caching-session-store.js +6 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +15 -20
- package/dist/plugins/checkpoint-store-sql.js +8 -8
- package/dist/plugins/file-history-store-sql.js +3 -2
- package/dist/plugins/k8s-pod-reaper.d.ts +2 -8
- package/dist/plugins/k8s-pod-reaper.js +3 -3
- package/dist/plugins/local-checkpoint-store.d.ts +1 -11
- package/dist/plugins/local-checkpoint-store.js +0 -1
- package/dist/plugins/local-session-store.d.ts +66 -27
- package/dist/plugins/local-session-store.js +60 -28
- package/dist/plugins/mailbox-store-sql.d.ts +11 -3
- package/dist/plugins/mailbox-store-sql.js +103 -15
- package/dist/plugins/memory-sync-store-pg.d.ts +16 -5
- package/dist/plugins/memory-sync-store-tidb.d.ts +3 -3
- package/dist/plugins/one-time-migrations.d.ts +6 -3
- package/dist/plugins/one-time-migrations.js +3 -4
- package/dist/plugins/permission-rule-store-sql.js +21 -13
- package/dist/plugins/pg-session-storage.d.ts +10 -2
- package/dist/plugins/pg-session-storage.js +18 -5
- package/dist/plugins/remote-env-adb.d.ts +3 -1
- package/dist/plugins/remote-env-adb.js +1 -1
- package/dist/plugins/remote-env-device.d.ts +3 -1
- package/dist/plugins/remote-env-device.js +1 -1
- package/dist/plugins/remote-env-e2b.d.ts +2 -0
- package/dist/plugins/remote-env-e2b.js +1 -1
- package/dist/plugins/remote-env-host.d.ts +3 -1
- package/dist/plugins/remote-env-host.js +1 -1
- package/dist/plugins/remote-env-k8s.d.ts +3 -1
- package/dist/plugins/remote-env-k8s.js +1 -1
- package/dist/plugins/remote-env-local-docker.d.ts +2 -0
- package/dist/plugins/remote-env-local-docker.js +1 -1
- package/dist/plugins/remote-env-ssh.d.ts +3 -1
- package/dist/plugins/remote-env-ssh.js +1 -1
- package/dist/plugins/retention-store-sql.d.ts +14 -2
- package/dist/plugins/retention-store-sql.js +6 -0
- package/dist/plugins/session-fork.d.ts +10 -13
- package/dist/plugins/session-fork.js +5 -25
- package/dist/plugins/session-ownership.d.ts +7 -0
- package/dist/plugins/session-ownership.js +5 -0
- package/dist/plugins/session-workspace-cell.d.ts +17 -0
- package/dist/plugins/session-workspace-cell.js +38 -0
- package/dist/plugins/store-backend.js +7 -7
- package/dist/plugins/tidb-pool.js +6 -0
- package/dist/plugins/tidb-session-store.d.ts +17 -2
- package/dist/plugins/tidb-session-store.js +16 -5
- package/dist/plugins/tool-result-store-sql.d.ts +13 -6
- package/dist/plugins/tool-result-store-sql.js +8 -7
- package/dist/plugins/worktree-isolation.d.ts +3 -2
- package/dist/plugins/worktree-isolation.js +1 -7
- package/dist/project-identity.d.ts +1 -14
- package/dist/project-identity.js +0 -4
- package/dist/rules-consent.js +6 -4
- package/dist/run-local.js +12 -6
- package/dist/runs.d.ts +7 -1
- package/dist/runs.js +6 -1
- package/dist/sandbox-git-plane.d.ts +2 -2
- package/dist/server-secret-env.d.ts +6 -3
- package/dist/server-secret-env.js +4 -2
- package/dist/session-incarnation.d.ts +4 -5
- package/dist/session-incarnation.js +2 -2
- package/dist/session-workspace.d.ts +130 -0
- package/dist/session-workspace.js +83 -0
- package/dist/spec-fields.d.ts +1 -1
- package/dist/standing-answer-grant.d.ts +2 -13
- package/dist/standing-answer-grant.js +0 -7
- package/dist/task-a2a.d.ts +44 -14
- package/dist/task-a2a.js +46 -20
- package/dist/task-mcp.d.ts +33 -20
- package/dist/task-mcp.js +46 -22
- package/dist/tool-approval.d.ts +32 -8
- package/dist/tool-approval.js +49 -12
- package/dist/trace/core-keyset-guard.d.ts +33 -18
- package/dist/trace/engine-notice-wire.d.ts +1 -1
- package/dist/trace/engine-notice-wire.js +2 -0
- package/dist/trace/exit-seat.d.ts +20 -1
- package/dist/trace/exit-seat.js +3 -0
- package/dist/trace/extension-call-face.d.ts +33 -0
- package/dist/trace/extension-call-face.js +39 -0
- package/dist/trace/project.d.ts +25 -7
- package/dist/trace/project.js +29 -3
- package/dist/trace/redact.js +42 -13
- package/dist/trace/self-minted-wire.d.ts +129 -10
- package/dist/trace/self-minted-wire.js +62 -3
- package/dist/trace/wire-projection-faces.d.ts +6 -6
- package/dist/trace/wire-projection-faces.js +6 -5
- package/dist/user-authored-text.d.ts +2 -2
- package/dist/user-authored-text.js +16 -1
- package/dist/web-admin-client/center-request.d.ts +3 -2
- package/dist/web-admin-client/center-request.js +2 -3
- package/dist/web-admin-client/skills-mcp.d.ts +8 -4
- package/dist/web-admin-client/skills-mcp.js +31 -18
- package/dist/web-admin-client/types.d.ts +5 -0
- package/package.json +3 -3
- package/wire-changes/server@7.138.0.json +446 -0
- package/dist/http/mcp-remote-text.d.ts +0 -5
- package/dist/http/mcp-remote-text.js +0 -7
package/dist/task-a2a.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type A2aServerSpec, type CompliancePosture, type LockedKey } from "@sema-agent/core";
|
|
2
|
+
import { type A2aDraft, type PeerDesk, type PeerLease } from "./peer-seat.js";
|
|
2
3
|
import { type DeclarationRefusal } from "./request-declaration-refusal.js";
|
|
3
4
|
import type { StoredToolAxesRereadM19 } from "./stored-request-tool-axes-m19.js";
|
|
4
5
|
import { type PrincipalAxisInput } from "./deployment-shape.js";
|
|
@@ -21,18 +22,42 @@ export interface A2aInjectionDrop {
|
|
|
21
22
|
readonly reason: string;
|
|
22
23
|
readonly field: string;
|
|
23
24
|
}
|
|
24
|
-
/** The deployment-facing shape this module reads: the principal axis
|
|
25
|
-
export type A2aGateConfig = PrincipalAxisInput
|
|
25
|
+
/** The deployment-facing shape this module reads: the principal axis, the operator locks and the compliance posture. */
|
|
26
|
+
export type A2aGateConfig = PrincipalAxisInput & {
|
|
27
|
+
readonly lockedConfigKeys?: readonly LockedKey[];
|
|
28
|
+
readonly compliancePosture?: CompliancePosture;
|
|
29
|
+
};
|
|
26
30
|
/**
|
|
27
|
-
* Does THIS deployment honor caller-supplied per-request A2A peers?
|
|
28
|
-
*
|
|
31
|
+
* Does THIS deployment honor caller-supplied per-request A2A peers? The same three vetoes as `task-mcp.ts mcpInjectionHonored`, each with its own
|
|
32
|
+
* engine word: multi-tenant (deployment-shape row `multiTenant`: a tenant cannot point the shared worker at an agent), the operator lock `a2a`, and a
|
|
33
|
+
* compliance posture that denies `a2a_peers` (the engine refuses a task that declares a peer under either at prepare — advertising the bit would be
|
|
34
|
+
* "says yes, every use refused"). The capability bit and the request leg both read this predicate, so there is exactly one place where the answer can be wrong.
|
|
35
|
+
* A posture that cannot be resolved throws here (validated twice before this site; loud beats a quiet boolean, same as the MCP gate).
|
|
29
36
|
*/
|
|
30
37
|
export declare function a2aInjectionHonored(config: A2aGateConfig): boolean;
|
|
31
38
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
39
|
+
* 请求面一条 `a2aPeers[]` 对端在 wire 上的形(= `TaskRequestBody.a2aPeers` 的元素型):引擎的 `A2aServerSpec` 去掉凭据引用位、加上 `headers`(件)。
|
|
40
|
+
* 交给引擎之前一律经 {@link toA2aDraft} 拆成草稿 —— 引擎的声明收据对带 `headers` 的声明整条拒,件由部署的对端座在每次发送时作答。
|
|
34
41
|
*/
|
|
35
|
-
export
|
|
42
|
+
export type RequestA2aPeer = Omit<A2aServerSpec, "credentials"> & {
|
|
43
|
+
headers?: Record<string, string>;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* 请求体 `a2aPeers[i]` 的**处置表** —— 同时是键闭集(判序 = 书写序:名 → 对端地址 → 它的 agent card 地址 → 凭据头 → 身份头 → 技能收窄 → 轴覆写)与每个键的去处:
|
|
47
|
+
* `headers` 是件(进草稿,交给引擎的声明里不带),其余进声明。三道编译格:对请求形两向(`satisfies`:请求形多一个键而表没写 / 表里写一个请求形上没有的词 ⇒ 红);
|
|
48
|
+
* 对引擎的 `A2aServerSpec` 两道(引擎长出 / 退役一个键,或退役凭据引用位 ⇒ 红)。于是这张表是从型**派生**出来的数据,而不是谁记得去同步的清单。
|
|
49
|
+
*/
|
|
50
|
+
export declare const A2A_PEER_DISPOSITION: {
|
|
51
|
+
readonly name: "engine";
|
|
52
|
+
readonly url: "engine";
|
|
53
|
+
readonly cardUrl: "engine";
|
|
54
|
+
readonly headers: "seat";
|
|
55
|
+
readonly principalHeader: "engine";
|
|
56
|
+
readonly allowSkills: "engine";
|
|
57
|
+
readonly toolAxes: "engine";
|
|
58
|
+
};
|
|
59
|
+
/** 请求体 `a2aPeers[i]` 的键闭集 = 处置表的键(书写序即判序)。 */
|
|
60
|
+
export declare const A2A_PEER_ENTRY_KEYS: readonly ("name" | "toolAxes" | "url" | "principalHeader" | "cardUrl" | "allowSkills" | "headers")[];
|
|
36
61
|
/**
|
|
37
62
|
* **请求入口的一次读**(`boot/resolve-spec.ts` 在腿首铸一份,受理门与读器——续跑重放腿只有读器——都吃这一份):`body.a2aPeers` 若是数组,返回同形数组
|
|
38
63
|
* (每个对象条目换成它的 {@link PeerSnapshot};非对象条目原样;长度保真 —— 界外的条数仍是受理门撞帽拒与读器丢弃留声的依据,只快照界内的条目);不是数组(缺席 / `null` / 别的形)原样返回。
|
|
@@ -45,12 +70,14 @@ export declare function snapshotRequestA2a(raw: unknown, options: {
|
|
|
45
70
|
}): unknown;
|
|
46
71
|
/** 读这条对端的是哪条腿:`fresh` = 新提交;`resume` = 续跑重放升级前存下的请求体(`boot/resolve-spec.ts` 的 `opts.leg` 分流,同一个读法)。 */
|
|
47
72
|
type A2aRequestLeg = "fresh" | "resume";
|
|
73
|
+
/** 请求面对端 → 草稿:按 {@link A2A_PEER_DISPOSITION} 拆(`headers` 进件,其余进声明;拆法与 MCP 请求腿同一只 `splitDeclaration`)。件缺席或空 ⇒ 草稿不带件。纯函数,不登记。 */
|
|
74
|
+
export declare function toA2aDraft(peer: RequestA2aPeer): A2aDraft;
|
|
48
75
|
/**
|
|
49
76
|
* Shape-check the raw `body.a2aPeers`. `null` → a non-array (caller error; the route may 400). Otherwise the
|
|
50
|
-
* valid subset + the dropped (malformed/over-cap) entries as {@link A2aInjectionDrop} facts (peer name · reason · body coordinate).
|
|
77
|
+
* valid subset as drafts (nothing registered) + the dropped (malformed/over-cap) entries as {@link A2aInjectionDrop} facts (peer name · reason · body coordinate).
|
|
51
78
|
*/
|
|
52
79
|
export declare function validateRequestA2a(raw: unknown): {
|
|
53
|
-
ok:
|
|
80
|
+
ok: A2aDraft[];
|
|
54
81
|
dropped: A2aInjectionDrop[];
|
|
55
82
|
} | null;
|
|
56
83
|
/**
|
|
@@ -69,13 +96,14 @@ export declare function requestA2aDeclarationRefusal(list: unknown): Declaration
|
|
|
69
96
|
/** fresh 提交腿的 `a2aPeers` 门:{@link requestA2aDeclarationRefusal} 有答案 ⇒ 同步 400(码 / 句 / `unknownKeys` / 机读位原样)。 */
|
|
70
97
|
export declare function assertRequestA2aEntries(bodyA2a: unknown): void;
|
|
71
98
|
/**
|
|
72
|
-
* Merge GATED per-request
|
|
99
|
+
* Merge GATED per-request peer drafts OVER the deployment baseline (center/config): **select first, then mint**. The baseline WINS on a name
|
|
73
100
|
* clash — a caller can ADD a peer but can never SHADOW a configured one. This is load-bearing on THIS lane
|
|
74
101
|
* specifically: the peer name is the tool-namespace segment (`a2a__<peer>__<skill>`), so shadowing would let
|
|
75
|
-
* a caller keep the tool NAMES the model was told about while swapping the agent behind them.
|
|
102
|
+
* a caller keep the tool NAMES the model was told about while swapping the agent behind them. A draft that loses to the baseline never reaches
|
|
103
|
+
* the desk; each winner is minted once on `desk` under `lease` (`peer-seat.ts` `mintDeclaration`: the reference at the top level, zero header values).
|
|
76
104
|
* Returns the baseline unchanged when there is nothing to add.
|
|
77
105
|
*/
|
|
78
|
-
export declare function mergeRequestA2a(baseline: A2aServerSpec[] | undefined,
|
|
106
|
+
export declare function mergeRequestA2a(baseline: A2aServerSpec[] | undefined, drafts: readonly A2aDraft[], desk: Pick<PeerDesk, "declare">, lease: PeerLease): A2aServerSpec[] | undefined;
|
|
79
107
|
/**
|
|
80
108
|
* The single entry point the spec-builder calls: compute the effective `TaskSpec.a2a` = deployment baseline
|
|
81
109
|
* (`a2aForScenario(config.a2aPeers, scenario)`) + the caller's gated per-request peers. Off the honored lane
|
|
@@ -83,7 +111,9 @@ export declare function mergeRequestA2a(baseline: A2aServerSpec[] | undefined, g
|
|
|
83
111
|
* peers a closed gate dropped, so a shell never silently believes its declaration took effect (it also reads
|
|
84
112
|
* `capabilities.a2aInjection`).
|
|
85
113
|
*/
|
|
86
|
-
export declare function resolveRequestA2a(baseline: A2aServerSpec[] | undefined, bodyA2a: unknown, config: A2aGateConfig,
|
|
114
|
+
export declare function resolveRequestA2a(baseline: A2aServerSpec[] | undefined, bodyA2a: unknown, config: A2aGateConfig,
|
|
115
|
+
/** 对端座的 desk 与本腿的宿主租约(必填,与 MCP 请求腿同一对):胜出草稿在它下面铸成带引用的声明。 */
|
|
116
|
+
desk: Pick<PeerDesk, "declare">, lease: PeerLease, logger?: {
|
|
87
117
|
info?(m: string, meta?: unknown): void;
|
|
88
118
|
warn(m: string, meta?: unknown): void;
|
|
89
119
|
},
|
package/dist/task-a2a.js
CHANGED
|
@@ -1,18 +1,36 @@
|
|
|
1
1
|
import { types } from "node:util";
|
|
2
|
+
import { resolveComplianceDenies } from "@sema-agent/core";
|
|
2
3
|
import { describeNotStringArray, describeValueShape, findUrlCredential, isStringArray } from "@sema-agent/settings-schema";
|
|
3
4
|
import { errorKindOf } from "./observability/err-text.js";
|
|
4
|
-
import { readPlainStringRecord } from "./plain-string-record.js";
|
|
5
|
+
import { declarationKeys, readPlainStringRecord, splitDeclaration } from "./plain-string-record.js";
|
|
6
|
+
import { mintDeclaration, peerSentence } from "./peer-seat.js";
|
|
5
7
|
import { readRequestToolAxes } from "./request-tool-axes.js";
|
|
6
8
|
import { MAX_KEY_ECHO, echoKeyName, keyClosureClause, scanKeyNames } from "./request-key-closure.js";
|
|
7
9
|
import { urlCredentialHeaderExit, urlCredentialRefusalReason } from "./url-credential-refusal.js";
|
|
8
10
|
import { declarationEntryNotObject, declarationIssueFacts, declarationNameIssue, declarationOverCapFacts, declaredName, judgeDeclarationKeys, requestDeclarationRefusal, throwDeclarationRefusal, } from "./request-declaration-refusal.js";
|
|
11
|
+
import { principalHeaderOwnershipIssue } from "./task-mcp.js";
|
|
9
12
|
import { DEPLOYMENT_SHAPE_PREDICATES } from "./deployment-shape.js";
|
|
10
13
|
export const MAX_REQUEST_A2A_PEERS = 32;
|
|
11
14
|
export function a2aInjectionHonored(config) {
|
|
15
|
+
if (config.lockedConfigKeys?.includes(A2A_LOCK_WORD) === true)
|
|
16
|
+
return false;
|
|
17
|
+
if (config.compliancePosture !== undefined && resolveComplianceDenies(config.compliancePosture).has(A2A_COMPLIANCE_WORD))
|
|
18
|
+
return false;
|
|
12
19
|
return !DEPLOYMENT_SHAPE_PREDICATES.multiTenant.holds(config);
|
|
13
20
|
}
|
|
21
|
+
const A2A_LOCK_WORD = "a2a";
|
|
22
|
+
const A2A_COMPLIANCE_WORD = "a2a_peers";
|
|
14
23
|
const HTTP_URL_RE = /^https?:\/\//i;
|
|
15
|
-
export const
|
|
24
|
+
export const A2A_PEER_DISPOSITION = {
|
|
25
|
+
name: "engine",
|
|
26
|
+
url: "engine",
|
|
27
|
+
cardUrl: "engine",
|
|
28
|
+
headers: "seat",
|
|
29
|
+
principalHeader: "engine",
|
|
30
|
+
allowSkills: "engine",
|
|
31
|
+
toolAxes: "engine",
|
|
32
|
+
};
|
|
33
|
+
export const A2A_PEER_ENTRY_KEYS = declarationKeys(A2A_PEER_DISPOSITION);
|
|
16
34
|
const A2A_PEER_ENTRY_KEY_SET = new Set(A2A_PEER_ENTRY_KEYS);
|
|
17
35
|
const NO_TOLERATED_KEYS = new Set();
|
|
18
36
|
const A2A_SEAT = {
|
|
@@ -133,11 +151,11 @@ export function snapshotRequestA2a(raw, options) {
|
|
|
133
151
|
return kept;
|
|
134
152
|
}
|
|
135
153
|
function toolAxesIssue(read) {
|
|
136
|
-
return { path: "toolAxes", reason: read.problem };
|
|
154
|
+
return { path: "toolAxes", reason: peerSentence(read.problem) };
|
|
137
155
|
}
|
|
138
156
|
function toolAxesNotCloneableIssue(read, clone) {
|
|
139
157
|
const because = clone === undefined || clone.ok ? "unavailable" : clone.because;
|
|
140
|
-
return { path: "toolAxes", reason: `declaration not cloneable (${because}) — the one-time migration re-reads the stored declaration as plain JSON data and this one is not; ${read.problem}` };
|
|
158
|
+
return { path: "toolAxes", reason: `declaration not cloneable (${because}) — the one-time migration re-reads the stored declaration as plain JSON data and this one is not; ${peerSentence(read.problem)}` };
|
|
141
159
|
}
|
|
142
160
|
const A2A_PEER_KEY_JUDGES = {
|
|
143
161
|
name: declarationNameIssue,
|
|
@@ -152,7 +170,11 @@ const A2A_PEER_KEY_JUDGES = {
|
|
|
152
170
|
const reserved = read.stripped[0];
|
|
153
171
|
return reserved === undefined ? undefined : { path: HEADERS_RESERVED_MEMBER_PATH, reason: reserved.fault };
|
|
154
172
|
},
|
|
155
|
-
principalHeader: (o) =>
|
|
173
|
+
principalHeader: (o) => o.principalHeader === undefined
|
|
174
|
+
? undefined
|
|
175
|
+
: typeof o.principalHeader !== "string"
|
|
176
|
+
? { path: "principalHeader", reason: `must be a string (a header name) — got ${describeValueShape(o.principalHeader)}` }
|
|
177
|
+
: principalHeaderOwnershipIssue(o.principalHeader, "a2a", "principalHeader"),
|
|
156
178
|
allowSkills: (o) => o.allowSkills === undefined || isStringArray(o.allowSkills)
|
|
157
179
|
? undefined
|
|
158
180
|
: { path: "allowSkills", reason: `must be an array of strings (omit the key to leave the peer's skills unrestricted) — got ${describeNotStringArray(o.allowSkills)}` },
|
|
@@ -162,10 +184,10 @@ const A2A_PEER_KEY_JUDGES = {
|
|
|
162
184
|
},
|
|
163
185
|
};
|
|
164
186
|
function normalizeA2aPeer(e, leg, at, storedToolAxes) {
|
|
165
|
-
const dropBecause = (issue) => ({
|
|
187
|
+
const dropBecause = (issue) => ({ draft: null, because: declarationIssueFacts(at, issue) });
|
|
166
188
|
const notObject = declarationEntryNotObject(A2A_SEAT, e, at);
|
|
167
189
|
if (notObject !== undefined)
|
|
168
|
-
return {
|
|
190
|
+
return { draft: null, because: { field: at, reason: notObject } };
|
|
169
191
|
const o = snapshotPeer(e, storedToolAxes !== undefined);
|
|
170
192
|
for (const key of A2A_PEER_ENTRY_KEYS) {
|
|
171
193
|
const issue = A2A_PEER_KEY_JUDGES[key](o, at);
|
|
@@ -206,12 +228,16 @@ function normalizeA2aPeer(e, leg, at, storedToolAxes) {
|
|
|
206
228
|
return unread;
|
|
207
229
|
if (again.toolAxes !== undefined)
|
|
208
230
|
spec.toolAxes = again.toolAxes;
|
|
209
|
-
return { spec, toolAxesReread: axes.problem, ...lost };
|
|
231
|
+
return { draft: toA2aDraft(spec), toolAxesReread: peerSentence(axes.problem), ...lost };
|
|
210
232
|
}
|
|
211
233
|
if (axes.toolAxes !== undefined)
|
|
212
234
|
spec.toolAxes = axes.toolAxes;
|
|
213
235
|
}
|
|
214
|
-
return { spec, ...lost };
|
|
236
|
+
return { draft: toA2aDraft(spec), ...lost };
|
|
237
|
+
}
|
|
238
|
+
export function toA2aDraft(peer) {
|
|
239
|
+
const split = splitDeclaration(peer, A2A_PEER_DISPOSITION);
|
|
240
|
+
return { spec: split.engine, ...(split.pieces === undefined ? {} : { pieces: split.pieces }) };
|
|
215
241
|
}
|
|
216
242
|
export function validateRequestA2a(raw) {
|
|
217
243
|
const v = validateRequestA2aEntries(raw, "fresh");
|
|
@@ -228,12 +254,12 @@ function validateRequestA2aEntries(raw, leg, storedToolAxes) {
|
|
|
228
254
|
const headersLost = [];
|
|
229
255
|
for (const [i, e] of raw.slice(0, MAX_REQUEST_A2A_PEERS).entries()) {
|
|
230
256
|
const read = normalizeA2aPeer(e, leg, `${A2A_SEAT.list}[${i}]`, storedToolAxes);
|
|
231
|
-
if (read.
|
|
232
|
-
ok.push(read.
|
|
257
|
+
if (read.draft !== null) {
|
|
258
|
+
ok.push(read.draft);
|
|
233
259
|
if (read.toolAxesReread !== undefined)
|
|
234
|
-
toolAxesReread.push({ peer: read.spec.name,
|
|
260
|
+
toolAxesReread.push({ peer: read.draft.spec.name, sentence: read.toolAxesReread });
|
|
235
261
|
if (read.headersLost !== undefined)
|
|
236
|
-
headersLost.push({ peer: read.spec.name, lost: read.headersLost });
|
|
262
|
+
headersLost.push({ peer: read.draft.spec.name, lost: read.headersLost });
|
|
237
263
|
}
|
|
238
264
|
else
|
|
239
265
|
dropped.push({ peer: declaredName(e).slice(0, 128), ...read.because });
|
|
@@ -265,14 +291,14 @@ export function requestA2aDeclarationRefusal(list) {
|
|
|
265
291
|
export function assertRequestA2aEntries(bodyA2a) {
|
|
266
292
|
throwDeclarationRefusal(requestA2aDeclarationRefusal(bodyA2a));
|
|
267
293
|
}
|
|
268
|
-
export function mergeRequestA2a(baseline,
|
|
269
|
-
if (
|
|
294
|
+
export function mergeRequestA2a(baseline, drafts, desk, lease) {
|
|
295
|
+
if (drafts.length === 0)
|
|
270
296
|
return baseline;
|
|
271
297
|
const taken = new Set((baseline ?? []).map((p) => p.name));
|
|
272
|
-
const
|
|
273
|
-
return
|
|
298
|
+
const winners = drafts.filter((d) => !taken.has(d.spec.name));
|
|
299
|
+
return winners.length === 0 ? baseline : [...(baseline ?? []), ...winners.map((d) => mintDeclaration(desk, d, lease))];
|
|
274
300
|
}
|
|
275
|
-
export function resolveRequestA2a(baseline, bodyA2a, config, logger, leg = "fresh", storedToolAxes) {
|
|
301
|
+
export function resolveRequestA2a(baseline, bodyA2a, config, desk, lease, logger, leg = "fresh", storedToolAxes) {
|
|
276
302
|
if (bodyA2a === undefined || bodyA2a === null)
|
|
277
303
|
return baseline;
|
|
278
304
|
if (!a2aInjectionHonored(config)) {
|
|
@@ -286,7 +312,7 @@ export function resolveRequestA2a(baseline, bodyA2a, config, logger, leg = "fres
|
|
|
286
312
|
}
|
|
287
313
|
if (v.dropped.length > 0)
|
|
288
314
|
logger?.warn("a2a_injection_dropped", { dropped: v.dropped });
|
|
289
|
-
const merged = mergeRequestA2a(baseline, v.ok);
|
|
315
|
+
const merged = mergeRequestA2a(baseline, v.ok, desk, lease);
|
|
290
316
|
const baselineNames = new Set((baseline ?? []).map((p) => p.name));
|
|
291
317
|
for (const r of v.toolAxesReread) {
|
|
292
318
|
if (baselineNames.has(r.peer))
|
|
@@ -294,7 +320,7 @@ export function resolveRequestA2a(baseline, bodyA2a, config, logger, leg = "fres
|
|
|
294
320
|
logger?.warn("a2a_tool_axes_read_as_before_upgrade", {
|
|
295
321
|
peer: r.peer,
|
|
296
322
|
migration: "M19",
|
|
297
|
-
problem: r.
|
|
323
|
+
problem: r.sentence,
|
|
298
324
|
note: "this replayed request was stored before the upgrade and the peer's toolAxes declaration does not pass the engine's reader; while one-time migration M19 is open it is read the way the pre-upgrade reader reads it (members that do not fit are dropped) and the peer stays mounted — once the window closes the same request leaves this peer unmounted",
|
|
299
325
|
});
|
|
300
326
|
}
|
package/dist/task-mcp.d.ts
CHANGED
|
@@ -24,7 +24,8 @@
|
|
|
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
|
|
27
|
+
import { type McpDraft, type PeerDesk, type PeerLease } from "./peer-seat.js";
|
|
28
|
+
import { type DeclarationKeyIssue, type DeclarationRefusal } from "./request-declaration-refusal.js";
|
|
28
29
|
import type { StoredToolAxesRereadM19 } from "./stored-request-tool-axes-m19.js";
|
|
29
30
|
/** Bound on caller-supplied servers (anti-DoS; a real local `.mcp.json` has a handful). */
|
|
30
31
|
export declare const MAX_REQUEST_MCP_SERVERS = 32;
|
|
@@ -52,11 +53,11 @@ export declare const MAX_REQUEST_MCP_DROP_NAMES = 32;
|
|
|
52
53
|
* 再写一份就是一份手抄投影([ref]:任何仓第一排查点),而两份哪天分岔,窄的那一份会把**合法**的键当成
|
|
53
54
|
* 拼写错误 400 掉。
|
|
54
55
|
*
|
|
55
|
-
* ## 🔴
|
|
56
|
-
*
|
|
57
|
-
* ⇒
|
|
58
|
-
*
|
|
59
|
-
*
|
|
56
|
+
* ## 🔴 两向钉在请求面条目形上(不是手抄)
|
|
57
|
+
* 条目层:下面四道编译期门两向钉在请求面条目形 {@link RequestMcpServerSpec}(条目层 = 引擎 `McpServerSpec` 的条目层)上:型上多一个键而表没跟 ⇒ 红;表里写一个型上没有的词
|
|
58
|
+
* (改名 / 拼错 / 删键后的残条目)⇒ 也红。传输层:两张键表**就是**两臂处置表的键(`mcp-assertion-header.ts` 的 {@link MCP_STDIO_TRANSPORT_DISPOSITION} /
|
|
59
|
+
* {@link MCP_HTTP_TRANSPORT_DISPOSITION}),处置表对契约包的请求传输形两向钉、对引擎的传输形再钉两道 —— 一张表同时说「收哪些键」与「每个键去哪」。
|
|
60
|
+
* 于是这几张表是从型**派生**出来的数据,而不是谁记得去同步的清单。
|
|
60
61
|
*
|
|
61
62
|
* ## 一个回答者,两扇门;读器另算
|
|
62
63
|
* 「这份声明能不能原样用」只有 {@link requestMcpDeclarationRefusal} 一只回答者:词表外的键(条目层 / 本形 `transport` 层)⇒ `400 request.body_shape` 带 `unknownKeys`,
|
|
@@ -66,12 +67,11 @@ export declare const MAX_REQUEST_MCP_DROP_NAMES = 32;
|
|
|
66
67
|
* ## 受理键的判官表也钉在这张表上
|
|
67
68
|
* 逐键判官表 {@link ENTRY_KEY_JUDGES} 是 `{ [K in 本表的键]: 判官 }` 的映射型 —— 本表多一个键而没写判官 ⇒ 编译红,不会出现「这个键写坏了却被悄悄受理」的新键。
|
|
68
69
|
*/
|
|
69
|
-
export declare const MCP_SERVER_ENTRY_KEYS: readonly ["allowTools", "contentOrigin", "elicitation", "name", "source", "startupTimeoutMs", "toolAxes", "toolFaces", "transport"];
|
|
70
|
-
/** `transport.kind:"stdio"` 的键闭集(
|
|
71
|
-
export declare const STDIO_MCP_TRANSPORT_KEYS: readonly
|
|
72
|
-
/** `transport.kind:"http"` 的键闭集(
|
|
73
|
-
|
|
74
|
-
export declare const HTTP_MCP_TRANSPORT_KEYS: readonly ["assertionHeader", "headers", "kind", "principalHeader", "url"];
|
|
70
|
+
export declare const MCP_SERVER_ENTRY_KEYS: readonly ["allowTools", "contentOrigin", "elicitation", "name", "sessionMetaKey", "source", "startupTimeoutMs", "toolAxes", "toolFaces", "transport"];
|
|
71
|
+
/** `transport.kind:"stdio"` 的键闭集 = stdio 臂处置表的键(`env` 是件,其余进声明)。 */
|
|
72
|
+
export declare const STDIO_MCP_TRANSPORT_KEYS: readonly ("kind" | "command" | "args" | "env")[];
|
|
73
|
+
/** `transport.kind:"http"` 的键闭集 = http 臂处置表的键(`headers` 是件,`assertionHeader` 承载不发送,其余进声明)。 */
|
|
74
|
+
export declare const HTTP_MCP_TRANSPORT_KEYS: readonly ("kind" | "url" | "principalHeader" | "headers" | "assertionHeader")[];
|
|
75
75
|
/**
|
|
76
76
|
* Does THIS deployment honor caller-supplied per-request MCP servers? TRUE for a SINGLE-USER deployment
|
|
77
77
|
* (deployment-shape row `multiTenant` does not hold) — the requester is the super-admin of their own worker, so their `.mcp.json` is
|
|
@@ -157,9 +157,10 @@ export declare function requestEntryIdentityIssue(entry: unknown): McpIdentityHe
|
|
|
157
157
|
* `startupTimeoutMs` = 引擎根导出的 `mcpStartupTimeoutMs`(坏值抛 `config.mcp_startup_timeout_invalid`,本门把那一句作原因句);`contentOrigin` = `isContentOriginWord`;`source` = `isMcpSourceLabel`;
|
|
158
158
|
* `name` = `isUsableWireName`;`allowTools` / `transport.args` = {@link isStringArray};`transport.env` / `headers` = 契约包读器 {@link readRequestMcpTransport} 的纯数据记录读法;`elicitation` = {@link isElicitationFlag};
|
|
159
159
|
* `transport` 的硬字段(`kind` / `command` / `url`)与软字段(`args` / `env` / `headers` / `principalHeader`)= {@link readRequestMcpTransport}(读器投影 transport 的那一只函数,门读它报的 `lost`;读器在契约包,本仓只据 `lost[0]` 拒、据 `lost` 在重放腿逐处点名);
|
|
160
|
-
* 读器交回的 http `url` 再问「带不带凭据」= 契约包根的 `findUrlCredential`([ref];拒体另带机读 `field` + `reason`,见 {@link transportUrlCredentialIssue})
|
|
160
|
+
* 读器交回的 http `url` 再问「带不带凭据」= 契约包根的 `findUrlCredential`([ref];拒体另带机读 `field` + `reason`,见 {@link transportUrlCredentialIssue});
|
|
161
|
+
* http `principalHeader` 再问「这个名有没有别的属主」= 引擎包根的 `headerOwnershipCollision`(见 {@link principalHeaderOwnershipIssue})。
|
|
161
162
|
* 于是判官受理的条目,读器({@link normalizeServer})交回的就是申报本身(只差判官自己的合法归一:空容器 / `skillScopeEligible:false` 折成缺席、`toolAxes` 取引擎的拷贝)——
|
|
162
|
-
* 探测口拨的那一份因此就是提交腿会挂的那一份,不另比一遍(另比就是第二只回答者)。唯一按门分的后果是「声明了 `assertionHeader`」这一个事实(
|
|
163
|
+
* 探测口拨的那一份因此就是提交腿会挂的那一份,不另比一遍(另比就是第二只回答者)。唯一按门分的后果是「声明了 `assertionHeader`」这一个事实(本部署不发送断言,
|
|
163
164
|
* 承载、不发送):新提交照挂 + 运维 info,探测口整条拒 —— 判据是同一只谓词 `specWithholdsMcpAssertionHeader`(开关 `MCP_ASSERTION_HEADER_SENT` 假时「声明了」即「被扣下」),不是第二份判法(见 `readMcpProbeBody`)。
|
|
164
165
|
*
|
|
165
166
|
* ## 射程
|
|
@@ -184,6 +185,14 @@ export declare function requestMcpDeclarationRefusal(list: unknown): Declaration
|
|
|
184
185
|
* 交出去的剔掉 `__proto__` 名成员),server 照挂,运维 warn `mcp_transport_members_dropped` 逐处点名 —— 续跑重放腿不静默丢声明。
|
|
185
186
|
*/
|
|
186
187
|
export declare function assertRequestMcpEntries(bodyMcp: unknown): void;
|
|
188
|
+
/**
|
|
189
|
+
* `principalHeader` 的**名**有没有别的属主:判官是引擎包根的 `headerOwnershipCollision`(拨号时判同一条的那一只;这里只问身份头这一个名,件头名由引擎在发送时判)——
|
|
190
|
+
* 名(任何大小写)是这一协议的传输自己写的头(MCP:`mcp-session-id` · `mcp-protocol-version` · `content-type` · `accept` · `last-event-id`;A2A 没有),或是引擎的断言头
|
|
191
|
+
* (`connector-assertion`)⇒ 这一台拨号时必被拒,新提交在门上按名拒。原因句只点名**属主的拼写**(引擎 / 协议的词),不回显调用方写的串。
|
|
192
|
+
* 新提交腿上按名 400(MCP 门表的 `transport`、A2A 判官表的 `principalHeader`)。续跑重放腿不 4xx:MCP 的读器不问它 —— 存量体里这一形照挂,由引擎在拨号时拒那一台、兄弟照挂;
|
|
193
|
+
* A2A 的读器问同一张判官表 —— 这一条对端整条不进、`a2a_injection_dropped` 点名原因(引擎物化时同样拒那一台;结局同,原因说在本仓这一行)。
|
|
194
|
+
*/
|
|
195
|
+
export declare function principalHeaderOwnershipIssue(principalHeader: string | undefined, protocol: "mcp" | "a2a", path: string): DeclarationKeyIssue | undefined;
|
|
187
196
|
/**
|
|
188
197
|
* The ONE predicate for "this MCP server name belongs to the deployment": the scenario baseline (what mounted for
|
|
189
198
|
* this scenario) ∪ every name the deployment DECLARED (`CenterMcp.reservedNames` — mounted or not, any scenario).
|
|
@@ -194,12 +203,14 @@ export declare function assertRequestMcpEntries(bodyMcp: unknown): void;
|
|
|
194
203
|
*/
|
|
195
204
|
export declare function deploymentClosedMcpNames(baseline: McpServerSpec[] | undefined, reservedNames: readonly string[]): ReadonlySet<string>;
|
|
196
205
|
/**
|
|
197
|
-
* Merge GATED per-request MCP
|
|
198
|
-
* ({@link deploymentClosedMcpNames}) are dropped from `
|
|
199
|
-
*
|
|
200
|
-
*
|
|
206
|
+
* Merge GATED per-request MCP drafts OVER the deployment baseline (center/config): **select first, then mint**. Names closed by the deployment
|
|
207
|
+
* ({@link deploymentClosedMcpNames}) are dropped from `drafts` before anything is registered — a draft that loses to the deployment never
|
|
208
|
+
* reaches the desk; each winner is minted once on `desk` under `lease` (`peer-seat.ts` `mintDeclaration`: the reference on its transport, zero
|
|
209
|
+
* piece values). `desk` / `lease` = the process's peer desk and this leg's host lease (`boot/resolve-spec.ts` passes `peerDesk` and `opts.lease`).
|
|
210
|
+
* `reservedNames` has NO default: a security predicate's input is never implied — a caller with no deployment declaration passes `[]` and says so.
|
|
211
|
+
* Returns the baseline unchanged when nothing is added.
|
|
201
212
|
*/
|
|
202
|
-
export declare function mergeRequestMcp(baseline: McpServerSpec[] | undefined,
|
|
213
|
+
export declare function mergeRequestMcp(baseline: McpServerSpec[] | undefined, drafts: readonly McpDraft[], reservedNames: readonly string[], desk: Pick<PeerDesk, "declare">, lease: PeerLease): McpServerSpec[] | undefined;
|
|
203
214
|
/**
|
|
204
215
|
* The single entry point the spec-builder calls: compute the effective `TaskSpec.mcp` = deployment baseline + the
|
|
205
216
|
* caller's gated per-request servers. Off the honored lane (or with no caller servers) returns the baseline
|
|
@@ -216,7 +227,9 @@ export declare function resolveRequestMcp(baseline: McpServerSpec[] | undefined,
|
|
|
216
227
|
mcpServers?: {
|
|
217
228
|
reservedNames: readonly string[];
|
|
218
229
|
};
|
|
219
|
-
},
|
|
230
|
+
},
|
|
231
|
+
/** 对端座的 desk 与本腿的宿主租约(必填):胜出草稿在它下面铸成带引用的声明 —— 一条腿交给引擎的请求面声明只经它带引用。 */
|
|
232
|
+
desk: Pick<PeerDesk, "declare">, lease: PeerLease, logger?: {
|
|
220
233
|
info?(m: string, meta?: unknown): void;
|
|
221
234
|
warn(m: string, meta?: unknown): void;
|
|
222
235
|
},
|
package/dist/task-mcp.js
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
|
-
import { mcpInjectionDroppedNotice, mcpStartupTimeoutMs, readToolFaces, resolveComplianceDenies } from "@sema-agent/core";
|
|
1
|
+
import { MCP_ASSERTION_HEADER, headerOwnershipCollision, judgeSessionMetaKey, mcpInjectionDroppedNotice, mcpStartupTimeoutMs, readToolFaces, resolveComplianceDenies } from "@sema-agent/core";
|
|
2
2
|
import { MCP_CONTENT_ORIGINS, describeBadContentOrigin, describeNotStringArray, describeValueShape, findUrlCredential, isContentOriginWord, isStringArray, isUsableWireName, readRequestMcpTransport, stripControlAndBidi, } from "@sema-agent/settings-schema";
|
|
3
3
|
import { HttpError } from "./security.js";
|
|
4
4
|
import { GOVERNANCE_HTTP_STATUS } from "./governance-http-status.js";
|
|
5
5
|
import { DEPLOYMENT_SHAPE_PREDICATES } from "./deployment-shape.js";
|
|
6
6
|
import { MCP_SOURCE_LABEL_MAX, isMcpSourceLabel, readMcpSourceLabel } from "./mcp-source-label.js";
|
|
7
|
-
import { describeMcpAssertionHeaderNotAString, describeMcpIdentityHeadersConflict, mcpIdentityHeadersIssue, noteMcpAssertionHeaderIgnored, noteMcpAssertionHeaderNotSent, specWithholdsMcpAssertionHeader,
|
|
7
|
+
import { MCP_HTTP_TRANSPORT_DISPOSITION, MCP_STDIO_TRANSPORT_DISPOSITION, describeMcpAssertionHeaderNotAString, describeMcpIdentityHeadersConflict, mcpIdentityHeadersIssue, noteMcpAssertionHeaderIgnored, noteMcpAssertionHeaderNotSent, specWithholdsMcpAssertionHeader, toMcpDraft, } from "./mcp-assertion-header.js";
|
|
8
|
+
import { declarationKeys } from "./plain-string-record.js";
|
|
9
|
+
import { mintDeclaration, peerSentence } from "./peer-seat.js";
|
|
8
10
|
import { MAX_KEY_ECHO, echoKeyName, keyClosureClause, scanKeyClosure } from "./request-key-closure.js";
|
|
9
11
|
import { readRequestToolAxes } from "./request-tool-axes.js";
|
|
10
12
|
import { urlCredentialHeaderExit, urlCredentialRefusalReason } from "./url-credential-refusal.js";
|
|
11
13
|
import { MALFORMED_NAME, declarationFieldRefusal, declarationNameIssue, declaredName, judgeDeclarationKeys, requestDeclarationRefusal, throwDeclarationRefusal, } from "./request-declaration-refusal.js";
|
|
12
14
|
export const MAX_REQUEST_MCP_SERVERS = 32;
|
|
13
15
|
export const MAX_REQUEST_MCP_DROP_NAMES = MAX_REQUEST_MCP_SERVERS;
|
|
14
|
-
export const MCP_SERVER_ENTRY_KEYS = ["allowTools", "contentOrigin", "elicitation", "name", "source", "startupTimeoutMs", "toolAxes", "toolFaces", "transport"];
|
|
15
|
-
export const STDIO_MCP_TRANSPORT_KEYS =
|
|
16
|
-
export const HTTP_MCP_TRANSPORT_KEYS =
|
|
16
|
+
export const MCP_SERVER_ENTRY_KEYS = ["allowTools", "contentOrigin", "elicitation", "name", "sessionMetaKey", "source", "startupTimeoutMs", "toolAxes", "toolFaces", "transport"];
|
|
17
|
+
export const STDIO_MCP_TRANSPORT_KEYS = declarationKeys(MCP_STDIO_TRANSPORT_DISPOSITION);
|
|
18
|
+
export const HTTP_MCP_TRANSPORT_KEYS = declarationKeys(MCP_HTTP_TRANSPORT_DISPOSITION);
|
|
17
19
|
export function mcpInjectionHonored(config) {
|
|
18
20
|
if (config.lockedConfigKeys?.includes("mcp") === true)
|
|
19
21
|
return false;
|
|
@@ -52,7 +54,7 @@ function normalizeServer(e, noteBadOrigin, storedToolAxes) {
|
|
|
52
54
|
const again = readRequestToolAxes(storedToolAxes.mcp(o.toolAxes), "mcp");
|
|
53
55
|
if (again.ok) {
|
|
54
56
|
judged = again;
|
|
55
|
-
toolAxesReread = stripControlAndBidi(axes.problem);
|
|
57
|
+
toolAxesReread = peerSentence(stripControlAndBidi(axes.problem));
|
|
56
58
|
}
|
|
57
59
|
}
|
|
58
60
|
if (!judged.ok)
|
|
@@ -73,6 +75,11 @@ function normalizeServer(e, noteBadOrigin, storedToolAxes) {
|
|
|
73
75
|
return bad("toolFaces");
|
|
74
76
|
if (faces.faces !== undefined)
|
|
75
77
|
spec.toolFaces = faces.faces;
|
|
78
|
+
const metaKey = judgeSessionMetaKey(o.sessionMetaKey);
|
|
79
|
+
if ("problem" in metaKey)
|
|
80
|
+
return bad("sessionMetaKey");
|
|
81
|
+
if (metaKey.key !== undefined)
|
|
82
|
+
spec.sessionMetaKey = metaKey.key;
|
|
76
83
|
return { ok: true, spec, ...(toolAxesReread !== undefined ? { toolAxesReread } : {}), transportLost: read.lost };
|
|
77
84
|
}
|
|
78
85
|
export function validateRequestMcp(raw) {
|
|
@@ -92,7 +99,7 @@ function validateRequestMcpEntries(raw, storedToolAxes) {
|
|
|
92
99
|
if (v.ok) {
|
|
93
100
|
ok.push(v.spec);
|
|
94
101
|
if (v.toolAxesReread !== undefined)
|
|
95
|
-
toolAxesReread.push({ server: v.spec.name,
|
|
102
|
+
toolAxesReread.push({ server: v.spec.name, sentence: v.toolAxesReread });
|
|
96
103
|
if (v.transportLost.length > 0)
|
|
97
104
|
transportLost.push({ server: v.spec.name, lost: v.transportLost.map((i) => i.path) });
|
|
98
105
|
}
|
|
@@ -164,6 +171,17 @@ function transportUrlCredentialIssue(url) {
|
|
|
164
171
|
return undefined;
|
|
165
172
|
return { path: "transport.url", reason: urlCredentialRefusalReason(found, urlCredentialHeaderExit("transport.headers")), credential: found };
|
|
166
173
|
}
|
|
174
|
+
export function principalHeaderOwnershipIssue(principalHeader, protocol, path) {
|
|
175
|
+
if (principalHeader === undefined)
|
|
176
|
+
return undefined;
|
|
177
|
+
const collision = headerOwnershipCollision({ principalHeader }, protocol);
|
|
178
|
+
if (collision === undefined)
|
|
179
|
+
return undefined;
|
|
180
|
+
return {
|
|
181
|
+
path,
|
|
182
|
+
reason: `names the header "${collision.first}" (header names are case-insensitive), which is not the principal header's to own — it is ${protocol === "mcp" && collision.first !== MCP_ASSERTION_HEADER ? "a header the MCP transport writes itself" : "the engine's connector assertion header"}; name the principal header something of its own`,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
167
185
|
const ENTRY_KEY_JUDGES = {
|
|
168
186
|
allowTools: (o) => o.allowTools === undefined || isStringArray(o.allowTools)
|
|
169
187
|
? undefined
|
|
@@ -177,6 +195,10 @@ const ENTRY_KEY_JUDGES = {
|
|
|
177
195
|
},
|
|
178
196
|
elicitation: (o) => (o.elicitation === undefined || isElicitationFlag(o.elicitation) ? undefined : { path: "elicitation", reason: `must be a boolean — got ${describeValueShape(o.elicitation)}` }),
|
|
179
197
|
name: declarationNameIssue,
|
|
198
|
+
sessionMetaKey: (o) => {
|
|
199
|
+
const read = judgeSessionMetaKey(o.sessionMetaKey);
|
|
200
|
+
return "problem" in read ? { path: "sessionMetaKey", reason: peerSentence(read.problem) } : undefined;
|
|
201
|
+
},
|
|
180
202
|
source: (o) => o.source === undefined || isMcpSourceLabel(o.source)
|
|
181
203
|
? undefined
|
|
182
204
|
: { path: "source", reason: `must be a non-empty string of at most ${MCP_SOURCE_LABEL_MAX} characters (a display label) — got ${describeValueShape(o.source)}` },
|
|
@@ -190,16 +212,16 @@ const ENTRY_KEY_JUDGES = {
|
|
|
190
212
|
catch (e) {
|
|
191
213
|
if (e.code !== "config.mcp_startup_timeout_invalid")
|
|
192
214
|
throw e;
|
|
193
|
-
return { path: "startupTimeoutMs", reason: e instanceof Error ? e.message : String(e) };
|
|
215
|
+
return { path: "startupTimeoutMs", reason: peerSentence(e instanceof Error ? e.message : String(e)) };
|
|
194
216
|
}
|
|
195
217
|
},
|
|
196
218
|
toolAxes: (o) => {
|
|
197
219
|
const read = readRequestToolAxes(o.toolAxes, "mcp");
|
|
198
|
-
return read.ok ? undefined : { path: "toolAxes", reason: read.problem };
|
|
220
|
+
return read.ok ? undefined : { path: "toolAxes", reason: peerSentence(read.problem) };
|
|
199
221
|
},
|
|
200
222
|
toolFaces: (o) => {
|
|
201
223
|
const read = readToolFaces(o.toolFaces);
|
|
202
|
-
return read.ok ? undefined : { path: "toolFaces", reason: read.problem };
|
|
224
|
+
return read.ok ? undefined : { path: "toolFaces", reason: peerSentence(read.problem) };
|
|
203
225
|
},
|
|
204
226
|
transport: (o) => {
|
|
205
227
|
const read = readRequestMcpTransport(o.transport);
|
|
@@ -207,7 +229,9 @@ const ENTRY_KEY_JUDGES = {
|
|
|
207
229
|
return read.issue;
|
|
208
230
|
if (read.lost[0] !== undefined)
|
|
209
231
|
return read.lost[0];
|
|
210
|
-
|
|
232
|
+
if (read.transport.kind !== "http")
|
|
233
|
+
return undefined;
|
|
234
|
+
return transportUrlCredentialIssue(read.transport.url) ?? principalHeaderOwnershipIssue(read.transport.principalHeader, "mcp", "transport.principalHeader");
|
|
211
235
|
},
|
|
212
236
|
};
|
|
213
237
|
function requestEntryRefusal(entry, at) {
|
|
@@ -229,27 +253,27 @@ function requestEntryRefusal(entry, at) {
|
|
|
229
253
|
export function deploymentClosedMcpNames(baseline, reservedNames) {
|
|
230
254
|
return new Set([...(baseline ?? []).map((s) => s.name), ...reservedNames]);
|
|
231
255
|
}
|
|
232
|
-
export function mergeRequestMcp(baseline,
|
|
233
|
-
if (
|
|
256
|
+
export function mergeRequestMcp(baseline, drafts, reservedNames, desk, lease) {
|
|
257
|
+
if (drafts.length === 0)
|
|
234
258
|
return baseline;
|
|
235
259
|
const closed = deploymentClosedMcpNames(baseline, reservedNames);
|
|
236
|
-
const
|
|
237
|
-
return
|
|
260
|
+
const winners = drafts.filter((d) => !closed.has(d.spec.name));
|
|
261
|
+
return winners.length === 0 ? baseline : [...(baseline ?? []), ...winners.map((d) => mintDeclaration(desk, d, lease))];
|
|
238
262
|
}
|
|
239
|
-
export function resolveRequestMcp(baseline, bodyMcp, config, logger, notice, storedToolAxes) {
|
|
263
|
+
export function resolveRequestMcp(baseline, bodyMcp, config, desk, lease, logger, notice, storedToolAxes) {
|
|
240
264
|
if (bodyMcp === undefined || bodyMcp === null)
|
|
241
265
|
return baseline;
|
|
242
|
-
const { mcp, drops } = classifyRequestMcp(baseline, bodyMcp, config, logger, storedToolAxes);
|
|
266
|
+
const { mcp, drops } = classifyRequestMcp(baseline, bodyMcp, config, desk, lease, logger, storedToolAxes);
|
|
243
267
|
mintInjectionDropNotices(drops, notice);
|
|
244
268
|
return mcp;
|
|
245
269
|
}
|
|
246
|
-
function classifyRequestMcp(baseline, bodyMcp, config, logger, storedToolAxes) {
|
|
270
|
+
function classifyRequestMcp(baseline, bodyMcp, config, desk, lease, logger, storedToolAxes) {
|
|
247
271
|
if (bodyMcp !== undefined && bodyMcp !== null && !Array.isArray(bodyMcp)) {
|
|
248
272
|
logger?.warn("mcp_injection_ignored", { reason: "mcpServers not an array" });
|
|
249
273
|
return { mcp: baseline, drops: [] };
|
|
250
274
|
}
|
|
251
275
|
const entries = Array.isArray(bodyMcp) ? bodyMcp : [];
|
|
252
|
-
const outcome = mcpInjectionHonored(config) ? classifyHonored(baseline, entries, config, logger, storedToolAxes) : classifyGateClosed(baseline, entries);
|
|
276
|
+
const outcome = mcpInjectionHonored(config) ? classifyHonored(baseline, entries, config, desk, lease, logger, storedToolAxes) : classifyGateClosed(baseline, entries);
|
|
253
277
|
if (outcome.drops.length > 0)
|
|
254
278
|
logger?.warn("mcp_injection_dropped", { dropped: outcome.drops, droppedTotal: outcome.droppedTotal });
|
|
255
279
|
return { mcp: outcome.mcp, drops: outcome.drops };
|
|
@@ -261,7 +285,7 @@ function classifyGateClosed(baseline, entries) {
|
|
|
261
285
|
droppedTotal: entries.length,
|
|
262
286
|
};
|
|
263
287
|
}
|
|
264
|
-
function classifyHonored(baseline, entries, config, logger, storedToolAxes) {
|
|
288
|
+
function classifyHonored(baseline, entries, config, desk, lease, logger, storedToolAxes) {
|
|
265
289
|
const v = validateRequestMcpEntries(entries, storedToolAxes);
|
|
266
290
|
if (v.badContentOrigin.length > 0) {
|
|
267
291
|
logger?.warn("mcp_content_class_dropped", {
|
|
@@ -288,7 +312,7 @@ function classifyHonored(baseline, entries, config, logger, storedToolAxes) {
|
|
|
288
312
|
logger?.warn("mcp_tool_axes_read_as_before_upgrade", {
|
|
289
313
|
server: r.server,
|
|
290
314
|
migration: "M19",
|
|
291
|
-
problem: r.
|
|
315
|
+
problem: r.sentence,
|
|
292
316
|
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",
|
|
293
317
|
});
|
|
294
318
|
}
|
|
@@ -305,7 +329,7 @@ function classifyHonored(baseline, entries, config, logger, storedToolAxes) {
|
|
|
305
329
|
});
|
|
306
330
|
}
|
|
307
331
|
return {
|
|
308
|
-
mcp: mergeRequestMcp(baseline, v.ok.map(
|
|
332
|
+
mcp: mergeRequestMcp(baseline, v.ok.map(toMcpDraft), reservedNames, desk, lease),
|
|
309
333
|
drops: [...v.dropped, ...blocked.map((server) => ({ server, reason: "name_reserved_by_deployment" }))],
|
|
310
334
|
droppedTotal: v.droppedTotal + blocked.length,
|
|
311
335
|
};
|